mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51de4bf006 | ||
|
|
3b799ba5f9 | ||
|
|
127896246c | ||
|
|
2091f1282e | ||
|
|
5e916c324b | ||
|
|
81ab00a3cd | ||
|
|
a74f15c253 | ||
|
|
8721f78c05 | ||
|
|
315fff9e19 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"claude-dev": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add global workflows
|
||||||
+2
-1
@@ -57,7 +57,8 @@ message RefreshedRules {
|
|||||||
ClineRulesToggles local_cline_rules_toggles = 2;
|
ClineRulesToggles local_cline_rules_toggles = 2;
|
||||||
ClineRulesToggles local_cursor_rules_toggles = 3;
|
ClineRulesToggles local_cursor_rules_toggles = 3;
|
||||||
ClineRulesToggles local_windsurf_rules_toggles = 4;
|
ClineRulesToggles local_windsurf_rules_toggles = 4;
|
||||||
ClineRulesToggles workflow_toggles = 5;
|
ClineRulesToggles local_workflow_toggles = 5;
|
||||||
|
ClineRulesToggles global_workflow_toggles = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request to toggle a Windsurf rule
|
// Request to toggle a Windsurf rule
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
import { ensureRulesDirectoryExists, ensureWorkflowsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
@@ -172,9 +172,13 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
|
|||||||
try {
|
try {
|
||||||
let filePath: string
|
let filePath: string
|
||||||
if (isGlobal) {
|
if (isGlobal) {
|
||||||
// global means its implicitly clinerules
|
if (type === "workflow") {
|
||||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
const globalClineWorkflowFilePath = await ensureWorkflowsDirectoryExists()
|
||||||
filePath = path.join(globalClineRulesFilePath, filename)
|
filePath = path.join(globalClineWorkflowFilePath, filename)
|
||||||
|
} else {
|
||||||
|
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||||
|
filePath = path.join(globalClineRulesFilePath, filename)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||||
|
|
||||||
@@ -243,9 +247,15 @@ export async function deleteRuleFile(
|
|||||||
|
|
||||||
// Update the appropriate toggles
|
// Update the appropriate toggles
|
||||||
if (isGlobal) {
|
if (isGlobal) {
|
||||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
if (type === "workflow") {
|
||||||
delete toggles[rulePath]
|
const toggles = ((await getGlobalState(context, "globalWorkflowToggles")) as ClineRulesToggles) || {}
|
||||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
delete toggles[rulePath]
|
||||||
|
await updateGlobalState(context, "globalWorkflowToggles", toggles)
|
||||||
|
} else {
|
||||||
|
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||||
|
delete toggles[rulePath]
|
||||||
|
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (type === "workflow") {
|
if (type === "workflow") {
|
||||||
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { GlobalFileNames } from "@core/storage/disk"
|
import { GlobalFileNames, ensureWorkflowsDirectoryExists } from "@core/storage/disk"
|
||||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||||
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
import { getWorkspaceState, updateWorkspaceState, getGlobalState, updateGlobalState } from "@core/storage/state"
|
||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||||
|
|
||||||
@@ -11,10 +11,23 @@ import { synchronizeRuleToggles } from "@core/context/instructions/user-instruct
|
|||||||
export async function refreshWorkflowToggles(
|
export async function refreshWorkflowToggles(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
workingDirectory: string,
|
workingDirectory: string,
|
||||||
): Promise<ClineRulesToggles> {
|
): Promise<{
|
||||||
|
globalWorkflowToggles: ClineRulesToggles
|
||||||
|
localWorkflowToggles: ClineRulesToggles
|
||||||
|
}> {
|
||||||
|
// Global workflows
|
||||||
|
const globalWorkflowToggles = ((await getGlobalState(context, "globalWorkflowToggles")) as ClineRulesToggles) || {}
|
||||||
|
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
|
||||||
|
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
|
||||||
|
await updateGlobalState(context, "globalWorkflowToggles", updatedGlobalWorkflowToggles)
|
||||||
|
|
||||||
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||||
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
|
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
|
||||||
return updatedWorkflowToggles
|
|
||||||
|
return {
|
||||||
|
globalWorkflowToggles: updatedGlobalWorkflowToggles,
|
||||||
|
localWorkflowToggles: updatedWorkflowToggles,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
|
|||||||
try {
|
try {
|
||||||
const { globalToggles, localToggles } = await refreshClineRulesToggles(controller.context, cwd)
|
const { globalToggles, localToggles } = await refreshClineRulesToggles(controller.context, cwd)
|
||||||
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller.context, cwd)
|
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller.context, cwd)
|
||||||
const workflowToggles = await refreshWorkflowToggles(controller.context, cwd)
|
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller.context, cwd)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
globalClineRulesToggles: { toggles: globalToggles },
|
globalClineRulesToggles: { toggles: globalToggles },
|
||||||
localClineRulesToggles: { toggles: localToggles },
|
localClineRulesToggles: { toggles: localToggles },
|
||||||
localCursorRulesToggles: { toggles: cursorLocalToggles },
|
localCursorRulesToggles: { toggles: cursorLocalToggles },
|
||||||
localWindsurfRulesToggles: { toggles: windsurfLocalToggles },
|
localWindsurfRulesToggles: { toggles: windsurfLocalToggles },
|
||||||
workflowToggles: { toggles: workflowToggles },
|
localWorkflowToggles: { toggles: localWorkflowToggles },
|
||||||
|
globalWorkflowToggles: { toggles: globalWorkflowToggles },
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to refresh rules:", error)
|
console.error("Failed to refresh rules:", error)
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ import { fileExistsAtPath } from "@utils/fs"
|
|||||||
import { getWorkingState } from "@utils/git"
|
import { getWorkingState } from "@utils/git"
|
||||||
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
|
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
|
||||||
import { getTotalTasksSize } from "@utils/storage"
|
import { getTotalTasksSize } from "@utils/storage"
|
||||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
import {
|
||||||
|
ensureMcpServersDirectoryExists,
|
||||||
|
ensureSettingsDirectoryExists,
|
||||||
|
GlobalFileNames,
|
||||||
|
ensureWorkflowsDirectoryExists,
|
||||||
|
} from "../storage/disk"
|
||||||
import {
|
import {
|
||||||
getAllExtensionState,
|
getAllExtensionState,
|
||||||
getGlobalState,
|
getGlobalState,
|
||||||
@@ -381,12 +386,20 @@ export class Controller {
|
|||||||
// break
|
// break
|
||||||
// }
|
// }
|
||||||
case "toggleWorkflow": {
|
case "toggleWorkflow": {
|
||||||
const { workflowPath, enabled } = message
|
const { workflowPath, enabled, isGlobal } = message
|
||||||
if (workflowPath && typeof enabled === "boolean") {
|
if (workflowPath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
|
||||||
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
if (isGlobal) {
|
||||||
toggles[workflowPath] = enabled
|
const globalWorkflowToggles =
|
||||||
await updateWorkspaceState(this.context, "workflowToggles", toggles)
|
((await getGlobalState(this.context, "globalWorkflowToggles")) as ClineRulesToggles) || {}
|
||||||
await this.postStateToWebview()
|
globalWorkflowToggles[workflowPath] = enabled
|
||||||
|
await updateGlobalState(this.context, "globalWorkflowToggles", globalWorkflowToggles)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
} else {
|
||||||
|
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||||
|
toggles[workflowPath] = enabled
|
||||||
|
await updateWorkspaceState(this.context, "workflowToggles", toggles)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1259,6 +1272,7 @@ export class Controller {
|
|||||||
planActSeparateModelsSetting,
|
planActSeparateModelsSetting,
|
||||||
enableCheckpointsSetting,
|
enableCheckpointsSetting,
|
||||||
globalClineRulesToggles,
|
globalClineRulesToggles,
|
||||||
|
globalWorkflowToggles,
|
||||||
shellIntegrationTimeout,
|
shellIntegrationTimeout,
|
||||||
isNewUser,
|
isNewUser,
|
||||||
} = await getAllExtensionState(this.context)
|
} = await getAllExtensionState(this.context)
|
||||||
@@ -1272,7 +1286,7 @@ export class Controller {
|
|||||||
const localCursorRulesToggles =
|
const localCursorRulesToggles =
|
||||||
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||||
|
|
||||||
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
const localWorkflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: this.context.extension?.packageJSON?.version ?? "",
|
version: this.context.extension?.packageJSON?.version ?? "",
|
||||||
@@ -1301,7 +1315,8 @@ export class Controller {
|
|||||||
localClineRulesToggles: localClineRulesToggles || {},
|
localClineRulesToggles: localClineRulesToggles || {},
|
||||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||||
workflowToggles: workflowToggles || {},
|
localWorkflowToggles: localWorkflowToggles || {},
|
||||||
|
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||||
shellIntegrationTimeout,
|
shellIntegrationTimeout,
|
||||||
isNewUser,
|
isNewUser,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import fs from "fs/promises"
|
|||||||
*/
|
*/
|
||||||
export async function parseSlashCommands(
|
export async function parseSlashCommands(
|
||||||
text: string,
|
text: string,
|
||||||
workflowToggles: ClineRulesToggles,
|
localWorkflowToggles: ClineRulesToggles,
|
||||||
|
globalWorkflowToggles: ClineRulesToggles,
|
||||||
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
|
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
|
||||||
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
|
||||||
|
|
||||||
@@ -58,18 +59,29 @@ export async function parseSlashCommands(
|
|||||||
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
|
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// in practice we want to minimize this work, so we only do it if theres a possible match
|
const globalWorkflows = Object.entries(globalWorkflowToggles)
|
||||||
const enabledWorkflows = Object.entries(workflowToggles)
|
|
||||||
.filter(([_, enabled]) => enabled)
|
.filter(([_, enabled]) => enabled)
|
||||||
.map(([filePath, _]) => {
|
.map(([filePath, _]) => {
|
||||||
const fileName = filePath.replace(/^.*[/\\]/, "")
|
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullPath: filePath,
|
fullPath: filePath,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const localWorkflows = Object.entries(localWorkflowToggles)
|
||||||
|
.filter(([_, enabled]) => enabled)
|
||||||
|
.map(([filePath, _]) => {
|
||||||
|
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||||
|
return {
|
||||||
|
fullPath: filePath,
|
||||||
|
fileName: fileName,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// local workflows have precedence over global workflows
|
||||||
|
const enabledWorkflows = [...localWorkflows, ...globalWorkflows]
|
||||||
|
|
||||||
// Then check if the command matches any enabled workflow filename
|
// Then check if the command matches any enabled workflow filename
|
||||||
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
|
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,17 @@ export async function ensureRulesDirectoryExists(): Promise<string> {
|
|||||||
return clineRulesDir
|
return clineRulesDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ensureWorkflowsDirectoryExists(): Promise<string> {
|
||||||
|
const userDocumentsPath = await getDocumentsPath()
|
||||||
|
const clineWorkflowsDir = path.join(userDocumentsPath, "Cline", "Workflows")
|
||||||
|
try {
|
||||||
|
await fs.mkdir(clineWorkflowsDir, { recursive: true })
|
||||||
|
} catch (error) {
|
||||||
|
return path.join(os.homedir(), "Documents", "Cline", "Workflows") // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine because we will fail gracefully with a path that does not exist
|
||||||
|
}
|
||||||
|
return clineWorkflowsDir
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureMcpServersDirectoryExists(): Promise<string> {
|
export async function ensureMcpServersDirectoryExists(): Promise<string> {
|
||||||
const userDocumentsPath = await getDocumentsPath()
|
const userDocumentsPath = await getDocumentsPath()
|
||||||
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
|
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export type GlobalStateKey =
|
|||||||
| "openRouterProviderSorting"
|
| "openRouterProviderSorting"
|
||||||
| "autoApprovalSettings"
|
| "autoApprovalSettings"
|
||||||
| "globalClineRulesToggles"
|
| "globalClineRulesToggles"
|
||||||
|
| "globalWorkflowToggles"
|
||||||
| "browserSettings"
|
| "browserSettings"
|
||||||
| "chatSettings"
|
| "chatSettings"
|
||||||
| "vsCodeLmModelSelector"
|
| "vsCodeLmModelSelector"
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
|||||||
shellIntegrationTimeout,
|
shellIntegrationTimeout,
|
||||||
enableCheckpointsSettingRaw,
|
enableCheckpointsSettingRaw,
|
||||||
mcpMarketplaceEnabledRaw,
|
mcpMarketplaceEnabledRaw,
|
||||||
|
globalWorkflowToggles,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
@@ -251,6 +252,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
|||||||
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
|
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
|
||||||
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
|
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
|
||||||
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
|
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
|
||||||
|
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
|
||||||
fetch,
|
fetch,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -385,6 +387,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
|||||||
planActSeparateModelsSetting,
|
planActSeparateModelsSetting,
|
||||||
enableCheckpointsSetting: enableCheckpointsSetting,
|
enableCheckpointsSetting: enableCheckpointsSetting,
|
||||||
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
|
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
|
||||||
|
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4117,7 +4117,7 @@ export class Task {
|
|||||||
// Track if we need to check clinerulesFile
|
// Track if we need to check clinerulesFile
|
||||||
let needsClinerulesFileCheck = false
|
let needsClinerulesFileCheck = false
|
||||||
|
|
||||||
const workflowToggles = await refreshWorkflowToggles(this.getContext(), cwd)
|
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.getContext(), cwd)
|
||||||
|
|
||||||
const processUserContent = async () => {
|
const processUserContent = async () => {
|
||||||
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
|
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
|
||||||
@@ -4143,7 +4143,8 @@ export class Task {
|
|||||||
// when parsing slash commands, we still want to allow the user to provide their desired context
|
// when parsing slash commands, we still want to allow the user to provide their desired context
|
||||||
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
|
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
|
||||||
parsedText,
|
parsedText,
|
||||||
workflowToggles,
|
localWorkflowToggles,
|
||||||
|
globalWorkflowToggles,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (needsCheck) {
|
if (needsCheck) {
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ export interface ExtensionState {
|
|||||||
vscMachineId: string
|
vscMachineId: string
|
||||||
globalClineRulesToggles: ClineRulesToggles
|
globalClineRulesToggles: ClineRulesToggles
|
||||||
localClineRulesToggles: ClineRulesToggles
|
localClineRulesToggles: ClineRulesToggles
|
||||||
workflowToggles: ClineRulesToggles
|
localWorkflowToggles: ClineRulesToggles
|
||||||
|
globalWorkflowToggles: ClineRulesToggles
|
||||||
localCursorRulesToggles: ClineRulesToggles
|
localCursorRulesToggles: ClineRulesToggles
|
||||||
localWindsurfRulesToggles: ClineRulesToggles
|
localWindsurfRulesToggles: ClineRulesToggles
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,7 +259,15 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform, workflowToggles } = useExtensionState()
|
const {
|
||||||
|
filePaths,
|
||||||
|
chatSettings,
|
||||||
|
apiConfiguration,
|
||||||
|
openRouterModels,
|
||||||
|
platform,
|
||||||
|
localWorkflowToggles,
|
||||||
|
globalWorkflowToggles,
|
||||||
|
} = useExtensionState()
|
||||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||||
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
|
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
|
||||||
@@ -483,7 +491,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
setSelectedSlashCommandsIndex((prevIndex) => {
|
setSelectedSlashCommandsIndex((prevIndex) => {
|
||||||
const direction = event.key === "ArrowUp" ? -1 : 1
|
const direction = event.key === "ArrowUp" ? -1 : 1
|
||||||
// Get commands with workflow toggles
|
// Get commands with workflow toggles
|
||||||
const allCommands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
|
const allCommands = getMatchingSlashCommands(
|
||||||
|
slashCommandsQuery,
|
||||||
|
localWorkflowToggles,
|
||||||
|
globalWorkflowToggles,
|
||||||
|
)
|
||||||
|
|
||||||
if (allCommands.length === 0) {
|
if (allCommands.length === 0) {
|
||||||
return prevIndex
|
return prevIndex
|
||||||
@@ -501,7 +513,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
|
|
||||||
if ((event.key === "Enter" || event.key === "Tab") && selectedSlashCommandsIndex !== -1) {
|
if ((event.key === "Enter" || event.key === "Tab") && selectedSlashCommandsIndex !== -1) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const commands = getMatchingSlashCommands(slashCommandsQuery, workflowToggles)
|
const commands = getMatchingSlashCommands(slashCommandsQuery, localWorkflowToggles, globalWorkflowToggles)
|
||||||
if (commands.length > 0) {
|
if (commands.length > 0) {
|
||||||
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
|
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
|
||||||
}
|
}
|
||||||
@@ -904,7 +916,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
|
|
||||||
// extract and validate the exact command text
|
// extract and validate the exact command text
|
||||||
const commandText = processedText.substring(slashIndex + 1, endIndex)
|
const commandText = processedText.substring(slashIndex + 1, endIndex)
|
||||||
const isValidCommand = validateSlashCommand(commandText, workflowToggles)
|
const isValidCommand = validateSlashCommand(commandText, localWorkflowToggles, globalWorkflowToggles)
|
||||||
|
|
||||||
if (isValidCommand) {
|
if (isValidCommand) {
|
||||||
const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash
|
const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash
|
||||||
@@ -917,7 +929,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
highlightLayerRef.current.innerHTML = processedText
|
highlightLayerRef.current.innerHTML = processedText
|
||||||
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
|
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
|
||||||
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
|
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
|
||||||
}, [workflowToggles])
|
}, [localWorkflowToggles, globalWorkflowToggles])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
updateHighlights()
|
updateHighlights()
|
||||||
@@ -1400,7 +1412,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
setSelectedIndex={setSelectedSlashCommandsIndex}
|
setSelectedIndex={setSelectedSlashCommandsIndex}
|
||||||
onMouseDown={handleMenuMouseDown}
|
onMouseDown={handleMenuMouseDown}
|
||||||
query={slashCommandsQuery}
|
query={slashCommandsQuery}
|
||||||
workflowToggles={workflowToggles}
|
localWorkflowToggles={localWorkflowToggles}
|
||||||
|
globalWorkflowToggles={globalWorkflowToggles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ interface SlashCommandMenuProps {
|
|||||||
setSelectedIndex: (index: number) => void
|
setSelectedIndex: (index: number) => void
|
||||||
onMouseDown: () => void
|
onMouseDown: () => void
|
||||||
query: string
|
query: string
|
||||||
workflowToggles?: Record<string, boolean>
|
localWorkflowToggles?: Record<string, boolean>
|
||||||
|
globalWorkflowToggles?: Record<string, boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||||
@@ -16,7 +17,8 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
|||||||
setSelectedIndex,
|
setSelectedIndex,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
query,
|
query,
|
||||||
workflowToggles = {},
|
localWorkflowToggles = {},
|
||||||
|
globalWorkflowToggles = {},
|
||||||
}) => {
|
}) => {
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
|||||||
}, [selectedIndex])
|
}, [selectedIndex])
|
||||||
|
|
||||||
// Filter commands based on query
|
// Filter commands based on query
|
||||||
const filteredCommands = getMatchingSlashCommands(query, workflowToggles)
|
const filteredCommands = getMatchingSlashCommands(query, localWorkflowToggles, globalWorkflowToggles)
|
||||||
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
|
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
|
||||||
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
|
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ const ClineRulesToggleModal: React.FC = () => {
|
|||||||
localClineRulesToggles = {},
|
localClineRulesToggles = {},
|
||||||
localCursorRulesToggles = {},
|
localCursorRulesToggles = {},
|
||||||
localWindsurfRulesToggles = {},
|
localWindsurfRulesToggles = {},
|
||||||
workflowToggles = {},
|
localWorkflowToggles = {},
|
||||||
|
globalWorkflowToggles = {},
|
||||||
setGlobalClineRulesToggles,
|
setGlobalClineRulesToggles,
|
||||||
setLocalClineRulesToggles,
|
setLocalClineRulesToggles,
|
||||||
setLocalCursorRulesToggles,
|
setLocalCursorRulesToggles,
|
||||||
setLocalWindsurfRulesToggles,
|
setLocalWindsurfRulesToggles,
|
||||||
setWorkflowToggles,
|
setLocalWorkflowToggles,
|
||||||
|
setGlobalWorkflowToggles,
|
||||||
} = useExtensionState()
|
} = useExtensionState()
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const buttonRef = useRef<HTMLDivElement>(null)
|
const buttonRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -49,8 +51,11 @@ const ClineRulesToggleModal: React.FC = () => {
|
|||||||
if (response.localWindsurfRulesToggles?.toggles) {
|
if (response.localWindsurfRulesToggles?.toggles) {
|
||||||
setLocalWindsurfRulesToggles(response.localWindsurfRulesToggles.toggles)
|
setLocalWindsurfRulesToggles(response.localWindsurfRulesToggles.toggles)
|
||||||
}
|
}
|
||||||
if (response.workflowToggles?.toggles) {
|
if (response.localWorkflowToggles?.toggles) {
|
||||||
setWorkflowToggles(response.workflowToggles.toggles)
|
setLocalWorkflowToggles(response.localWorkflowToggles.toggles)
|
||||||
|
}
|
||||||
|
if (response.globalWorkflowToggles?.toggles) {
|
||||||
|
setGlobalWorkflowToggles(response.globalWorkflowToggles.toggles)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -77,7 +82,11 @@ const ClineRulesToggleModal: React.FC = () => {
|
|||||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
|
||||||
const workflows = Object.entries(workflowToggles || {})
|
const localWorkflows = Object.entries(localWorkflowToggles || {})
|
||||||
|
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
|
||||||
|
const globalWorkflows = Object.entries(globalWorkflowToggles || {})
|
||||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
|
||||||
@@ -133,11 +142,12 @@ const ClineRulesToggleModal: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleWorkflow = (workflowPath: string, enabled: boolean) => {
|
const toggleWorkflow = (isGlobal: boolean, workflowPath: string, enabled: boolean) => {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: "toggleWorkflow",
|
type: "toggleWorkflow",
|
||||||
workflowPath,
|
workflowPath,
|
||||||
enabled,
|
enabled,
|
||||||
|
isGlobal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,19 +309,35 @@ const ClineRulesToggleModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
/* Workflows section */
|
<>
|
||||||
<div style={{ marginBottom: -10 }}>
|
{/* Global Workflows Section */}
|
||||||
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
|
<div className="mb-3">
|
||||||
<RulesToggleList
|
<div className="text-sm font-normal mb-2">Global Workflows</div>
|
||||||
rules={workflows}
|
<RulesToggleList
|
||||||
toggleRule={toggleWorkflow}
|
rules={globalWorkflows}
|
||||||
listGap="small"
|
toggleRule={(rulePath, enabled) => toggleWorkflow(true, rulePath, enabled)}
|
||||||
isGlobal={false}
|
listGap="small"
|
||||||
ruleType={"workflow"}
|
isGlobal={true}
|
||||||
showNewRule={true}
|
ruleType={"workflow"}
|
||||||
showNoRules={false}
|
showNewRule={true}
|
||||||
/>
|
showNoRules={false}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Local Workflows Section */}
|
||||||
|
<div style={{ marginBottom: -10 }}>
|
||||||
|
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
|
||||||
|
<RulesToggleList
|
||||||
|
rules={localWorkflows}
|
||||||
|
toggleRule={(rulePath, enabled) => toggleWorkflow(false, rulePath, enabled)}
|
||||||
|
listGap="small"
|
||||||
|
isGlobal={false}
|
||||||
|
ruleType={"workflow"}
|
||||||
|
showNewRule={true}
|
||||||
|
showNoRules={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ interface ExtensionStateContextType extends ExtensionState {
|
|||||||
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||||
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
|
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
|
||||||
setLocalWindsurfRulesToggles: (toggles: Record<string, boolean>) => void
|
setLocalWindsurfRulesToggles: (toggles: Record<string, boolean>) => void
|
||||||
setWorkflowToggles: (toggles: Record<string, boolean>) => void
|
setLocalWorkflowToggles: (toggles: Record<string, boolean>) => void
|
||||||
|
setGlobalWorkflowToggles: (toggles: Record<string, boolean>) => void
|
||||||
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
|
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
|
||||||
|
|
||||||
// Navigation state setters
|
// Navigation state setters
|
||||||
@@ -161,7 +162,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
|||||||
localClineRulesToggles: {},
|
localClineRulesToggles: {},
|
||||||
localCursorRulesToggles: {},
|
localCursorRulesToggles: {},
|
||||||
localWindsurfRulesToggles: {},
|
localWindsurfRulesToggles: {},
|
||||||
workflowToggles: {},
|
localWorkflowToggles: {},
|
||||||
|
globalWorkflowToggles: {},
|
||||||
shellIntegrationTimeout: 4000, // default timeout for shell integration
|
shellIntegrationTimeout: 4000, // default timeout for shell integration
|
||||||
isNewUser: false,
|
isNewUser: false,
|
||||||
})
|
})
|
||||||
@@ -447,7 +449,8 @@ export const ExtensionStateContextProvider: React.FC<{
|
|||||||
localClineRulesToggles: state.localClineRulesToggles || {},
|
localClineRulesToggles: state.localClineRulesToggles || {},
|
||||||
localCursorRulesToggles: state.localCursorRulesToggles || {},
|
localCursorRulesToggles: state.localCursorRulesToggles || {},
|
||||||
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
|
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
|
||||||
workflowToggles: state.workflowToggles || {},
|
localWorkflowToggles: state.localWorkflowToggles || {},
|
||||||
|
globalWorkflowToggles: state.globalWorkflowToggles || {},
|
||||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||||
|
|
||||||
// Navigation functions
|
// Navigation functions
|
||||||
@@ -542,10 +545,15 @@ export const ExtensionStateContextProvider: React.FC<{
|
|||||||
...prevState,
|
...prevState,
|
||||||
localWindsurfRulesToggles: toggles,
|
localWindsurfRulesToggles: toggles,
|
||||||
})),
|
})),
|
||||||
setWorkflowToggles: (toggles) =>
|
setLocalWorkflowToggles: (toggles) =>
|
||||||
setState((prevState) => ({
|
setState((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
workflowToggles: toggles,
|
localWorkflowToggles: toggles,
|
||||||
|
})),
|
||||||
|
setGlobalWorkflowToggles: (toggles) =>
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
globalWorkflowToggles: toggles,
|
||||||
})),
|
})),
|
||||||
setMcpTab,
|
setMcpTab,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,19 +27,50 @@ export const DEFAULT_SLASH_COMMANDS: SlashCommand[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function getWorkflowCommands(workflowToggles: Record<string, boolean>): SlashCommand[] {
|
export function getWorkflowCommands(
|
||||||
return Object.entries(workflowToggles)
|
localWorkflowToggles: Record<string, boolean>,
|
||||||
|
globalWorkflowToggles: Record<string, boolean>,
|
||||||
|
): SlashCommand[] {
|
||||||
|
const { workflows: localWorkflows, nameSet: localWorkflowNames } = Object.entries(localWorkflowToggles)
|
||||||
.filter(([_, enabled]) => enabled)
|
.filter(([_, enabled]) => enabled)
|
||||||
.map(([filePath, _]) => {
|
.reduce(
|
||||||
// potentially remove the file extension if there is one, but this would then require
|
(acc, [filePath, _]) => {
|
||||||
// that we prevent users from having the same fname with different extensions
|
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||||
|
|
||||||
|
// Add to array of workflows
|
||||||
|
acc.workflows.push({
|
||||||
|
name: fileName,
|
||||||
|
section: "custom",
|
||||||
|
} as SlashCommand)
|
||||||
|
|
||||||
|
// Add to set of names
|
||||||
|
acc.nameSet.add(fileName)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{ workflows: [] as SlashCommand[], nameSet: new Set<string>() },
|
||||||
|
)
|
||||||
|
|
||||||
|
const globalWorkflows = Object.entries(globalWorkflowToggles)
|
||||||
|
.filter(([_, enabled]) => enabled)
|
||||||
|
.flatMap(([filePath, _]) => {
|
||||||
const fileName = filePath.replace(/^.*[/\\]/, "")
|
const fileName = filePath.replace(/^.*[/\\]/, "")
|
||||||
|
|
||||||
return {
|
// skip if a local workflow with the same name exists
|
||||||
name: fileName,
|
if (localWorkflowNames.has(fileName)) {
|
||||||
section: "custom",
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: fileName,
|
||||||
|
section: "custom",
|
||||||
|
},
|
||||||
|
] as SlashCommand[]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const workflows = [...localWorkflows, ...globalWorkflows]
|
||||||
|
return workflows
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regex for detecting slash commands in text
|
// Regex for detecting slash commands in text
|
||||||
@@ -102,8 +133,12 @@ export function shouldShowSlashCommandsMenu(text: string, cursorPosition: number
|
|||||||
/**
|
/**
|
||||||
* Gets filtered slash commands that match the current input
|
* Gets filtered slash commands that match the current input
|
||||||
*/
|
*/
|
||||||
export function getMatchingSlashCommands(query: string, workflowToggles: Record<string, boolean> = {}): SlashCommand[] {
|
export function getMatchingSlashCommands(
|
||||||
const workflowCommands = getWorkflowCommands(workflowToggles)
|
query: string,
|
||||||
|
localWorkflowToggles: Record<string, boolean> = {},
|
||||||
|
globalWorkflowToggles: Record<string, boolean> = {},
|
||||||
|
): SlashCommand[] {
|
||||||
|
const workflowCommands = getWorkflowCommands(localWorkflowToggles, globalWorkflowToggles)
|
||||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
@@ -134,12 +169,16 @@ export function insertSlashCommand(text: string, commandName: string): { newValu
|
|||||||
* Determines the validation state of a slash command
|
* Determines the validation state of a slash command
|
||||||
* Returns partial if we have a partial match against valid commands, or full for full match
|
* Returns partial if we have a partial match against valid commands, or full for full match
|
||||||
*/
|
*/
|
||||||
export function validateSlashCommand(command: string, workflowToggles: Record<string, boolean> = {}): "full" | "partial" | null {
|
export function validateSlashCommand(
|
||||||
|
command: string,
|
||||||
|
localWorkflowToggles: Record<string, boolean> = {},
|
||||||
|
globalWorkflowToggles: Record<string, boolean> = {},
|
||||||
|
): "full" | "partial" | null {
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowCommands = getWorkflowCommands(workflowToggles)
|
const workflowCommands = getWorkflowCommands(localWorkflowToggles, globalWorkflowToggles)
|
||||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands]
|
||||||
|
|
||||||
// case sensitive matching
|
// case sensitive matching
|
||||||
|
|||||||
Reference in New Issue
Block a user