Compare commits

...
Author SHA1 Message Date
0xtoshii 51de4bf006 changeset 2025-05-21 19:43:20 -07:00
Toshii 3b799ba5f9 Delete proto file 2025-05-21 18:17:15 -07:00
0xtoshii 127896246c protos deleted 2025-05-21 18:12:05 -07:00
0xtoshii 2091f1282e ui 2025-05-21 18:04:48 -07:00
0xtoshii 5e916c324b base 2 2025-05-21 15:39:06 -07:00
0xtoshii 81ab00a3cd protos 2025-05-21 15:18:43 -07:00
0xtoshii a74f15c253 nit 2025-05-21 15:18:28 -07:00
0xtoshii 8721f78c05 base 2025-05-21 15:13:48 -07:00
0xtoshii 315fff9e19 backend global workflows 2025-05-21 14:07:19 -07:00
17 changed files with 237 additions and 75 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add global workflows
+2 -1
View File
@@ -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,
}
} }
+3 -2
View File
@@ -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)
+24 -9
View File
@@ -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,
} }
+16 -4
View File
@@ -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)
+11
View File
@@ -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")
+1
View File
@@ -55,6 +55,7 @@ export type GlobalStateKey =
| "openRouterProviderSorting" | "openRouterProviderSorting"
| "autoApprovalSettings" | "autoApprovalSettings"
| "globalClineRulesToggles" | "globalClineRulesToggles"
| "globalWorkflowToggles"
| "browserSettings" | "browserSettings"
| "chatSettings" | "chatSettings"
| "vsCodeLmModelSelector" | "vsCodeLmModelSelector"
+3
View File
@@ -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 || {},
} }
} }
+3 -2
View File
@@ -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) {
+2 -1
View File
@@ -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,
} }
+51 -12
View File
@@ -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