mirror of
https://github.com/cline/cline.git
synced 2026-09-12 17:19:32 +08:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
816492b6d9 | ||
|
|
7dadb9c296 | ||
|
|
1421a65e91 | ||
|
|
6635f3b6f1 | ||
|
|
29c1237b41 | ||
|
|
d72bc30c9b |
@@ -15,6 +15,7 @@
|
||||
### Changed
|
||||
|
||||
- Polish `Notification` hook functionality
|
||||
- Remove the workflows feature; migrate recurring guidance to rules, skills, hooks, or MCP prompts
|
||||
|
||||
## [3.76.0]
|
||||
|
||||
|
||||
@@ -68,10 +68,6 @@ interface AppProps {
|
||||
localWindsurfRulesToggles?: Record<string, boolean>
|
||||
localAgentsRulesToggles?: Record<string, boolean>
|
||||
onToggleRule?: (isGlobal: boolean, rulePath: string, enabled: boolean, ruleType: string) => void
|
||||
// Workflow toggles
|
||||
globalWorkflowToggles?: Record<string, boolean>
|
||||
localWorkflowToggles?: Record<string, boolean>
|
||||
onToggleWorkflow?: (isGlobal: boolean, workflowPath: string, enabled: boolean) => void
|
||||
// Hooks
|
||||
hooksEnabled?: boolean
|
||||
globalHooks?: HookInfo[]
|
||||
@@ -123,10 +119,6 @@ const InternalApp: React.FC<AppProps> = ({
|
||||
localWindsurfRulesToggles,
|
||||
localAgentsRulesToggles,
|
||||
onToggleRule,
|
||||
// Workflows
|
||||
globalWorkflowToggles,
|
||||
localWorkflowToggles,
|
||||
onToggleWorkflow,
|
||||
// Hooks
|
||||
hooksEnabled,
|
||||
globalHooks,
|
||||
@@ -225,18 +217,15 @@ const InternalApp: React.FC<AppProps> = ({
|
||||
globalHooks={globalHooks}
|
||||
globalSkills={globalSkills}
|
||||
globalState={globalState}
|
||||
globalWorkflowToggles={globalWorkflowToggles}
|
||||
hooksEnabled={hooksEnabled}
|
||||
localAgentsRulesToggles={localAgentsRulesToggles}
|
||||
localClineRulesToggles={localClineRulesToggles}
|
||||
localCursorRulesToggles={localCursorRulesToggles}
|
||||
localSkills={localSkills}
|
||||
localWindsurfRulesToggles={localWindsurfRulesToggles}
|
||||
localWorkflowToggles={localWorkflowToggles}
|
||||
onToggleHook={onToggleHook}
|
||||
onToggleRule={onToggleRule}
|
||||
onToggleSkill={onToggleSkill}
|
||||
onToggleWorkflow={onToggleWorkflow}
|
||||
skillsEnabled={skillsEnabled}
|
||||
workspaceHooks={workspaceHooks}
|
||||
workspaceState={workspaceState}
|
||||
|
||||
@@ -133,7 +133,6 @@ vi.mock("../utils/slash-commands", async (importOriginal) => {
|
||||
extractSlashQuery: vi.fn(() => ({ inSlashMode: false, query: "", slashIndex: -1 })),
|
||||
filterCommands: vi.fn(() => []),
|
||||
insertSlashCommand: vi.fn((text: string) => text),
|
||||
sortCommandsWorkflowsFirst: vi.fn((cmds: unknown[]) => cmds),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ import {
|
||||
filterCommands,
|
||||
getStandaloneSlashCommandToExecute,
|
||||
insertSlashCommand,
|
||||
sortCommandsWorkflowsFirst,
|
||||
} from "../utils/slash-commands"
|
||||
import { waitFor } from "../utils/timeout"
|
||||
import { isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
@@ -626,7 +625,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
|
||||
// fetch completes. This avoids a race that can make the quit command tests
|
||||
// flaky on slower Windows CI runners.
|
||||
const cliOnlyCommands = createCliOnlySlashCommands()
|
||||
setAvailableCommands([...cliOnlyCommands, ...sortCommandsWorkflowsFirst(cliCommands)])
|
||||
setAvailableCommands([...cliOnlyCommands, ...cliCommands])
|
||||
} catch {
|
||||
// Keep CLI-only commands available even if backend command loading fails.
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Interactive config view component for displaying and editing configuration values
|
||||
* Supports tabs for Settings, Rules, Workflows, Hooks, and Skills
|
||||
* Supports tabs for Settings, Rules, Hooks, and Skills
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -55,10 +55,6 @@ interface ConfigViewProps {
|
||||
localWindsurfRulesToggles?: Record<string, boolean>
|
||||
localAgentsRulesToggles?: Record<string, boolean>
|
||||
onToggleRule?: (isGlobal: boolean, rulePath: string, enabled: boolean, ruleType: string) => void
|
||||
// Workflow toggles
|
||||
globalWorkflowToggles?: Record<string, boolean>
|
||||
localWorkflowToggles?: Record<string, boolean>
|
||||
onToggleWorkflow?: (isGlobal: boolean, workflowPath: string, enabled: boolean) => void
|
||||
// Hooks
|
||||
hooksEnabled?: boolean
|
||||
globalHooks?: HookInfo[]
|
||||
@@ -70,7 +66,7 @@ interface ConfigViewProps {
|
||||
localSkills?: SkillInfo[]
|
||||
onToggleSkill?: (isGlobal: boolean, skillPath: string, enabled: boolean) => void
|
||||
// Open folder callback
|
||||
onOpenFolder?: (folderType: "rules" | "workflows" | "hooks" | "skills", isGlobal: boolean) => void
|
||||
onOpenFolder?: (folderType: "rules" | "hooks" | "skills", isGlobal: boolean) => void
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -89,9 +85,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
localWindsurfRulesToggles,
|
||||
localAgentsRulesToggles,
|
||||
onToggleRule,
|
||||
globalWorkflowToggles,
|
||||
localWorkflowToggles,
|
||||
onToggleWorkflow,
|
||||
hooksEnabled,
|
||||
globalHooks = [],
|
||||
workspaceHooks = [],
|
||||
@@ -141,14 +134,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
localAgentsRulesToggles,
|
||||
])
|
||||
|
||||
// Build entries for workflows tab
|
||||
const workflowEntries = useMemo(() => {
|
||||
const entries: ToggleEntry[] = []
|
||||
entries.push(...buildToggleEntries(globalWorkflowToggles, "global"))
|
||||
entries.push(...buildToggleEntries(localWorkflowToggles, "workspace"))
|
||||
return entries
|
||||
}, [globalWorkflowToggles, localWorkflowToggles])
|
||||
|
||||
// Build flat list of hooks
|
||||
const hookEntries = useMemo(() => {
|
||||
const entries: { hook: HookInfo; isGlobal: boolean; workspaceName?: string }[] = []
|
||||
@@ -174,8 +159,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
return filteredConfigEntries.length
|
||||
case "rules":
|
||||
return ruleEntries.length
|
||||
case "workflows":
|
||||
return workflowEntries.length
|
||||
case "hooks":
|
||||
return hookEntries.length
|
||||
case "skills":
|
||||
@@ -183,14 +166,7 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}, [
|
||||
currentTab,
|
||||
filteredConfigEntries.length,
|
||||
ruleEntries.length,
|
||||
workflowEntries.length,
|
||||
hookEntries.length,
|
||||
skillEntries.length,
|
||||
])
|
||||
}, [currentTab, filteredConfigEntries.length, ruleEntries.length, hookEntries.length, skillEntries.length])
|
||||
|
||||
// Get available tabs
|
||||
const availableTabs = useMemo(() => {
|
||||
@@ -277,14 +253,11 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle handlers for rules/workflows/hooks/skills
|
||||
// Toggle handlers for rules/hooks/skills
|
||||
const handleToggle = () => {
|
||||
if (currentTab === "rules" && ruleEntries[selectedIndex] && onToggleRule) {
|
||||
const entry = ruleEntries[selectedIndex]
|
||||
onToggleRule(entry.source === "global", entry.path, !entry.enabled, entry.ruleType || "cline")
|
||||
} else if (currentTab === "workflows" && workflowEntries[selectedIndex] && onToggleWorkflow) {
|
||||
const entry = workflowEntries[selectedIndex]
|
||||
onToggleWorkflow(entry.source === "global", entry.path, !entry.enabled)
|
||||
} else if (currentTab === "hooks" && hookEntries[selectedIndex] && onToggleHook) {
|
||||
const entry = hookEntries[selectedIndex]
|
||||
onToggleHook(entry.isGlobal, entry.hook.name, !entry.hook.enabled, entry.workspaceName)
|
||||
@@ -359,24 +332,22 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
setSearchQuery((prev) => prev + input)
|
||||
}
|
||||
} else if (key.return || key.tab || input === " ") {
|
||||
// Toggle for rules/workflows/hooks/skills
|
||||
// Toggle for rules/hooks/skills
|
||||
handleToggle()
|
||||
}
|
||||
|
||||
// Open folder (for rules/workflows/hooks/skills tabs)
|
||||
// Open folder (for rules/hooks/skills tabs)
|
||||
if (input === "o" && onOpenFolder && currentTab !== "settings") {
|
||||
// Determine if current selection is global or workspace based on the selected entry
|
||||
let isGlobal = true
|
||||
if (currentTab === "rules" && ruleEntries[selectedIndex]) {
|
||||
isGlobal = ruleEntries[selectedIndex].source === "global"
|
||||
} else if (currentTab === "workflows" && workflowEntries[selectedIndex]) {
|
||||
isGlobal = workflowEntries[selectedIndex].source === "global"
|
||||
} else if (currentTab === "hooks" && hookEntries[selectedIndex]) {
|
||||
isGlobal = hookEntries[selectedIndex].isGlobal
|
||||
} else if (currentTab === "skills" && skillEntries[selectedIndex]) {
|
||||
isGlobal = skillEntries[selectedIndex].isGlobal
|
||||
}
|
||||
onOpenFolder(currentTab as "rules" | "workflows" | "hooks" | "skills", isGlobal)
|
||||
onOpenFolder(currentTab as "rules" | "hooks" | "skills", isGlobal)
|
||||
}
|
||||
},
|
||||
{ isActive: isRawModeSupported && !isEditing },
|
||||
@@ -511,37 +482,6 @@ export const ConfigView: React.FC<ConfigViewProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
case "workflows": {
|
||||
if (workflowEntries.length === 0) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color="gray">No workflows configured. Add workflow files to enable this feature.</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const visibleEntries = workflowEntries.slice(startIndex, startIndex + MAX_VISIBLE)
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{visibleEntries.map((entry, idx) => {
|
||||
const actualIndex = startIndex + idx
|
||||
const prevEntry = visibleEntries[idx - 1]
|
||||
const showHeader = !prevEntry || prevEntry.source !== entry.source
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${entry.source}-${entry.path}`}>
|
||||
{showHeader && (
|
||||
<SectionHeader
|
||||
title={entry.source === "global" ? "Global Workflows:" : "Workspace Workflows:"}
|
||||
/>
|
||||
)}
|
||||
<ToggleRow entry={entry} isSelected={actualIndex === selectedIndex} />
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
case "hooks": {
|
||||
if (hookEntries.length === 0) {
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useStdinContext } from "../context/StdinContext"
|
||||
// ============================================================================
|
||||
|
||||
export type ValueType = "string" | "number" | "boolean" | "object" | "undefined"
|
||||
export type TabView = "settings" | "rules" | "workflows" | "hooks" | "skills"
|
||||
export type TabView = "settings" | "rules" | "hooks" | "skills"
|
||||
|
||||
export interface ConfigEntry {
|
||||
key: string
|
||||
@@ -71,7 +71,6 @@ export const SEPARATOR = "─".repeat(80)
|
||||
export const TABS: { key: TabView; label: string; requiresFlag?: "hooks" | "skills" }[] = [
|
||||
{ key: "settings", label: "Settings" },
|
||||
{ key: "rules", label: "Rules" },
|
||||
{ key: "workflows", label: "Workflows" },
|
||||
{ key: "hooks", label: "Hooks", requiresFlag: "hooks" },
|
||||
{ key: "skills", label: "Skills", requiresFlag: "skills" },
|
||||
]
|
||||
|
||||
@@ -63,10 +63,6 @@ export const ConfigViewWrapper: React.FC<ConfigViewWrapperProps> = ({
|
||||
const [localWindsurfRulesToggles, setLocalWindsurfRulesToggles] = useState<Record<string, boolean>>({})
|
||||
const [localAgentsRulesToggles, setLocalAgentsRulesToggles] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Workflow state
|
||||
const [globalWorkflowToggles, setGlobalWorkflowToggles] = useState<Record<string, boolean>>({})
|
||||
const [localWorkflowToggles, setLocalWorkflowToggles] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Hooks state
|
||||
const [globalHooks, setGlobalHooks] = useState<HookInfo[]>([])
|
||||
const [workspaceHooksState, setWorkspaceHooksState] = useState<WorkspaceHooks[]>([])
|
||||
@@ -88,8 +84,6 @@ export const ConfigViewWrapper: React.FC<ConfigViewWrapperProps> = ({
|
||||
setLocalCursorRulesToggles(rulesData.localCursorRulesToggles?.toggles || {})
|
||||
setLocalWindsurfRulesToggles(rulesData.localWindsurfRulesToggles?.toggles || {})
|
||||
setLocalAgentsRulesToggles(rulesData.localAgentsRulesToggles?.toggles || {})
|
||||
setGlobalWorkflowToggles(rulesData.globalWorkflowToggles?.toggles || {})
|
||||
setLocalWorkflowToggles(rulesData.localWorkflowToggles?.toggles || {})
|
||||
|
||||
if (hooksEnabled) {
|
||||
const hooksData = await refreshHooks(controller, {})
|
||||
@@ -146,23 +140,6 @@ export const ConfigViewWrapper: React.FC<ConfigViewWrapperProps> = ({
|
||||
[controller],
|
||||
)
|
||||
|
||||
const handleToggleWorkflow = useCallback(
|
||||
async (isGlobal: boolean, workflowPath: string, enabled: boolean) => {
|
||||
const { toggleWorkflow } = await import("@/core/controller/file/toggleWorkflow")
|
||||
const scope = isGlobal ? RuleScope.GLOBAL : RuleScope.LOCAL
|
||||
|
||||
// Optimistic update
|
||||
if (isGlobal) {
|
||||
setGlobalWorkflowToggles((prev) => ({ ...prev, [workflowPath]: enabled }))
|
||||
} else {
|
||||
setLocalWorkflowToggles((prev) => ({ ...prev, [workflowPath]: enabled }))
|
||||
}
|
||||
|
||||
await toggleWorkflow(controller, { metadata: undefined, workflowPath, enabled, scope })
|
||||
},
|
||||
[controller],
|
||||
)
|
||||
|
||||
const handleToggleHook = useCallback(
|
||||
async (isGlobal: boolean, hookName: string, enabled: boolean, workspaceName?: string) => {
|
||||
const { toggleHook } = await import("@/core/controller/file/toggleHook")
|
||||
@@ -206,7 +183,7 @@ export const ConfigViewWrapper: React.FC<ConfigViewWrapperProps> = ({
|
||||
)
|
||||
|
||||
const handleOpenFolder = useCallback(
|
||||
async (folderType: "rules" | "workflows" | "hooks" | "skills", isGlobal: boolean) => {
|
||||
async (folderType: "rules" | "hooks" | "skills", isGlobal: boolean) => {
|
||||
let folderPath: string
|
||||
|
||||
if (isGlobal) {
|
||||
@@ -220,7 +197,7 @@ export const ConfigViewWrapper: React.FC<ConfigViewWrapperProps> = ({
|
||||
if (!primaryWorkspace) {
|
||||
return
|
||||
}
|
||||
// Local rules/workflows/hooks/skills are in .clinerules or .cline
|
||||
// Local rules/hooks/skills are in .clinerules or .cline
|
||||
const subFolder = folderType === "rules" ? "rules" : folderType
|
||||
folderPath = path.join(primaryWorkspace, ".clinerules", subFolder)
|
||||
}
|
||||
@@ -277,19 +254,16 @@ export const ConfigViewWrapper: React.FC<ConfigViewWrapperProps> = ({
|
||||
globalHooks={globalHooks}
|
||||
globalSkills={globalSkills}
|
||||
globalState={globalStateLocal}
|
||||
globalWorkflowToggles={globalWorkflowToggles}
|
||||
hooksEnabled={hooksEnabled}
|
||||
localAgentsRulesToggles={localAgentsRulesToggles}
|
||||
localClineRulesToggles={localClineRulesToggles}
|
||||
localCursorRulesToggles={localCursorRulesToggles}
|
||||
localSkills={localSkills}
|
||||
localWindsurfRulesToggles={localWindsurfRulesToggles}
|
||||
localWorkflowToggles={localWorkflowToggles}
|
||||
onOpenFolder={handleOpenFolder}
|
||||
onToggleHook={handleToggleHook}
|
||||
onToggleRule={handleToggleRule}
|
||||
onToggleSkill={handleToggleSkill}
|
||||
onToggleWorkflow={handleToggleWorkflow}
|
||||
onUpdateGlobal={handleUpdateGlobal}
|
||||
onUpdateWorkspace={handleUpdateWorkspace}
|
||||
skillsEnabled={skillsEnabled}
|
||||
|
||||
@@ -57,13 +57,6 @@ export function getVisibleWindow<T>(items: T[], selectedIndex: number, maxVisibl
|
||||
return { items: items.slice(startIndex, endIndex), startIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort commands with workflows (custom section) first, then default commands.
|
||||
*/
|
||||
export function sortCommandsWorkflowsFirst(commands: SlashCommandInfo[]): SlashCommandInfo[] {
|
||||
return [...commands.filter((cmd) => cmd.section === "custom"), ...commands.filter((cmd) => cmd.section !== "custom")]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract slash command query from input text.
|
||||
* Returns info about whether we're in slash mode and what the query is.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"workflowToggles": {},
|
||||
"localClineRulesToggles": {},
|
||||
"localWindsurfRulesToggles": {},
|
||||
"localCursorRulesToggles": {},
|
||||
|
||||
@@ -36,14 +36,6 @@ Manage Cline rules that guide AI behavior:
|
||||
|
||||
Rules help Cline understand your project's conventions, coding standards, and preferences.
|
||||
|
||||
### Workflows Tab
|
||||
|
||||
View and manage [workflows](/customization/workflows):
|
||||
|
||||
- List available workflows
|
||||
- View workflow definitions
|
||||
- Workflows appear as slash commands in interactive mode
|
||||
|
||||
### Hooks Tab
|
||||
|
||||
Configure [hooks](/customization/hooks) for custom logic integration:
|
||||
|
||||
@@ -98,13 +98,7 @@ Type `/` to see available commands. Slash commands provide quick access to setti
|
||||
| `/help` | Show help and available commands |
|
||||
| `/exit` | Exit the CLI |
|
||||
|
||||
### Workflow Commands
|
||||
|
||||
If you have [workflows](/customization/workflows) configured, they appear as additional slash commands. For example, if you have a workflow named `code-review`, you can invoke it with:
|
||||
|
||||
```text
|
||||
/code-review
|
||||
```
|
||||
The slash-command menu also includes compatible built-in commands and any available MCP prompt commands returned by your configured servers.
|
||||
|
||||
## Settings Panel
|
||||
|
||||
|
||||
@@ -311,6 +311,6 @@ When using these templates:
|
||||
4. **Test your own instructions.** Follow your guide from scratch to catch missing steps.
|
||||
|
||||
<Tip>
|
||||
Use the `/write-docs` workflow to generate documentation from these templates automatically.
|
||||
Cline helps you fill in each section based on your project.
|
||||
Use these templates with Cline to generate documentation drafts.
|
||||
Cline can help you fill in each section based on your project.
|
||||
</Tip>
|
||||
|
||||
@@ -6,16 +6,16 @@ description: "How to write and contribute to Cline documentation"
|
||||
|
||||
Cline's documentation lives in the `docs/` directory and uses [Mintlify](https://mintlify.com) for rendering. This guide covers how to write docs that match Cline's established style.
|
||||
|
||||
## Using the Documentation Workflow
|
||||
## Using Cline to Draft Documentation
|
||||
|
||||
The fastest way to create documentation is using the `/write-docs` workflow. Type `/write-docs` in Cline and describe what you want to document. Cline guides you through a 4-step process:
|
||||
The fastest way to create documentation is to ask Cline directly and use the templates in this guide. A practical 4-step process is:
|
||||
|
||||
1. **Research**: Examine existing docs structure and patterns
|
||||
2. **Scope**: Clarify audience, doc type, and key use cases
|
||||
3. **Outline**: Select a template and create structure
|
||||
4. **Write**: Generate documentation following style guidelines
|
||||
|
||||
The workflow file lives at `.clinerules/workflows/write-docs.md` and contains templates, style rules, and examples.
|
||||
The templates and examples in this guide support each of those steps.
|
||||
|
||||
## Documentation Principles
|
||||
|
||||
@@ -194,7 +194,7 @@ Open `http://localhost:3000` to see your changes in real time.
|
||||
<Card title="Documentation Templates" icon="file-lines" href="/contributing/doc-templates">
|
||||
Templates for different documentation types.
|
||||
</Card>
|
||||
<Card title="Workflows" icon="diagram-project" href="/customization/workflows">
|
||||
Learn about Cline's workflow system.
|
||||
<Card title="Customization Overview" icon="sliders" href="/customization/overview">
|
||||
Understand how rules, skills, hooks, and .clineignore fit together.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Using Commands"
|
||||
sidebarTitle: "Using Commands"
|
||||
description: "Built-in slash commands to manage context, plan implementations, and create reusable workflows."
|
||||
description: "Built-in slash commands to manage context, plan implementations, and navigate key Cline actions."
|
||||
---
|
||||
|
||||
Cline provides slash commands in chat that help you manage your conversation and plan complex implementations.
|
||||
@@ -67,9 +67,3 @@ Use `/explain-changes` when reviewing code, onboarding to a new codebase, or und
|
||||
`/reportbug` collects diagnostic information and helps you report issues with Cline. It gathers relevant context like your configuration, recent errors, and system details to make bug reports more useful for the development team.
|
||||
|
||||
Use `/reportbug` when you encounter unexpected behavior, crashes, or bugs you want to report.
|
||||
|
||||
## Custom Workflows
|
||||
|
||||
Beyond the built-in slash commands, you can create your own workflow files that work the same way. Store Markdown files in `.clinerules/workflows/` and invoke them with `/your-workflow.md`.
|
||||
|
||||
For a complete guide on creating and managing custom workflows, see [Workflows](/customization/workflows).
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Understand how Rules, Skills, Workflows, Hooks, and .clineignore work together to customize Cline."
|
||||
description: "Understand how Rules, Skills, Hooks, and .clineignore work together to customize Cline."
|
||||
---
|
||||
|
||||
Out of the box, Cline is a general-purpose AI assistant. Customizations transform it into an expert on your codebase, your team's conventions, and your workflows. Instead of repeating the same instructions every task, you define them once and Cline follows them automatically.
|
||||
|
||||
Cline offers five systems for this: Rules, Skills, Workflows, Hooks, and .clineignore. Each serves a different purpose and activates at different times.
|
||||
Cline offers four systems for this: Rules, Skills, Hooks, and .clineignore. Each serves a different purpose and activates at different times.
|
||||
|
||||
## Quick Comparison
|
||||
|
||||
@@ -14,7 +14,6 @@ Cline offers five systems for this: Rules, Skills, Workflows, Hooks, and .clinei
|
||||
|---------|---------|-------------|----------|
|
||||
| **[Rules](/customization/cline-rules)** | Define how Cline behaves | Always (or contextually) | Coding standards, project constraints, team conventions |
|
||||
| **[Skills](/customization/skills)** | Domain expertise loaded on-demand | Triggered by matching requests | Specialized knowledge, complex procedures, institutional expertise |
|
||||
| **[Workflows](/customization/workflows)** | Step-by-step task automation | Invoked with `/workflow.md` | Repetitive processes, release procedures, setup scripts |
|
||||
| **[Hooks](/customization/hooks)** | Inject custom logic at key moments | Automatically on specific events | Validation, enforcement, monitoring, automation triggers |
|
||||
| **[.clineignore](/customization/clineignore)** | Control file access | Always | Excluding dependencies, build artifacts, large data files |
|
||||
|
||||
@@ -24,31 +23,27 @@ Cline offers five systems for this: Rules, Skills, Workflows, Hooks, and .clinei
|
||||
|
||||
**[Skills](/customization/skills)** are domain expertise that loads only when relevant. Use them when you have extensive knowledge that would waste context if always active. Cline sees skill descriptions at startup and activates the full instructions only when your request matches. A data analysis skill might include pandas patterns, visualization preferences, and output formats that Cline only loads when you're working with data files.
|
||||
|
||||
**[Workflows](/customization/workflows)** are explicit task scripts you invoke on demand. Use them when you have a repeatable multi-step process that should run the same way every time. Type `/release.md` and Cline executes your release sequence: bump version, run tests, update changelog, commit, tag, push. Workflows define *what* to do, step by step.
|
||||
|
||||
**[Hooks](/customization/hooks)** are programmatic guardrails that run automatically at key moments. Use them when you need to validate, enforce, or extend Cline's behavior with custom code. A hook might block `.js` file creation in a TypeScript project, run linters before saves, or notify external services after deployments.
|
||||
|
||||
**[.clineignore](/customization/clineignore)** controls which files and directories Cline can access. Use it to exclude dependencies, build artifacts, generated files, and large data files from Cline's context. This reduces token usage, lowers costs, and keeps Cline focused on the code that matters. It works like `.gitignore`: add patterns to a `.clineignore` file in your project root and matching files are automatically excluded.
|
||||
|
||||
### Example: A Release Process
|
||||
|
||||
Consider how all five work together for releasing a new version:
|
||||
Consider how these systems work together for releasing a new version:
|
||||
|
||||
1. **Rules** ensure Cline follows your team's commit message format and versioning policy
|
||||
2. **Skills** offer deep knowledge about your CI/CD system that Cline loads when deployment questions arise
|
||||
3. **Workflows** provide the explicit `/release.md` sequence: bump version, update changelog, tag, push
|
||||
4. **Hooks** validate that tests pass before allowing any commit or that the changelog was actually updated
|
||||
5. **.clineignore** keeps build artifacts, `node_modules/`, and generated files out of Cline's context so it stays focused
|
||||
3. **Hooks** validate that tests pass before allowing any commit or that the changelog was actually updated
|
||||
4. **.clineignore** keeps build artifacts, `node_modules/`, and generated files out of Cline's context so it stays focused
|
||||
|
||||
## Storage Locations
|
||||
|
||||
All five systems support both global and project-specific configurations:
|
||||
These systems support both global and project-specific configurations where applicable:
|
||||
|
||||
| System | Global Location | Project Location |
|
||||
|--------|-----------------|------------------|
|
||||
| Rules | `~/Documents/Cline/Rules/` | `.clinerules/` |
|
||||
| Skills | `~/.cline/skills/` | `.cline/skills/` |
|
||||
| Workflows | `~/Documents/Cline/Workflows/` | `.clinerules/workflows/` |
|
||||
| Hooks | `~/Documents/Cline/Hooks/` | `.clinerules/hooks/` |
|
||||
| .clineignore | N/A | `.clineignore` |
|
||||
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
---
|
||||
title: "Workflows"
|
||||
sidebarTitle: "Workflows"
|
||||
description: "Automate repetitive tasks with Markdown-based workflow files."
|
||||
---
|
||||
|
||||
Workflows are Markdown files that define a series of steps to guide Cline through repetitive or complex tasks. Type `/` followed by the workflow's filename to invoke it (e.g., `/deploy.md`).
|
||||
|
||||
Deploying, setting up a new project, running through a release checklist: these tasks often require remembering a dozen steps, running commands in the right order, and updating files manually. Mess up one step and you're debugging for an hour. Workflows turn those multi-step processes into one command. Type `/release.md` and Cline handles the version bump, runs tests, updates the changelog, commits, tags, and pushes. You just review and approve.
|
||||
|
||||
## Workflow Structure
|
||||
|
||||
A workflow is a markdown file with a title and steps. The filename becomes the command: `demo-workflow.md` is invoked with `/demo-workflow.md`.
|
||||
|
||||
````markdown title="demo-workflow.md"
|
||||
# Demo Workflow
|
||||
|
||||
Brief description of what this workflow accomplishes.
|
||||
|
||||
## Step 1: Check prerequisites
|
||||
Verify the environment is ready. Look for required tools and dependencies.
|
||||
|
||||
## Step 2: Run the build
|
||||
Execute the build command:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Step 3: Verify results
|
||||
Check that the build completed successfully and report any issues.
|
||||
````
|
||||
|
||||
Steps can be written at different levels of detail:
|
||||
|
||||
- **High-level**: "Run the test suite and fix any failures" lets Cline decide how to accomplish the goal
|
||||
- **Specific**: Use XML tool syntax or exact commands when you need precise control
|
||||
|
||||
## Creating Workflows
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the Workflows menu">
|
||||
Click the scale icon at the bottom of the Cline panel, to the left of the model selector. Switch to the Workflows tab.
|
||||
</Step>
|
||||
<Step title="Create a new workflow file">
|
||||
Click "New workflow file..." and enter a filename (e.g., `deploy`). The file will be created with a `.md` extension.
|
||||
</Step>
|
||||
<Step title="Write your workflow">
|
||||
Add a title and numbered steps in markdown format. Describe what each step should accomplish.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
**Create workflows from completed tasks.** After finishing something you'll need to repeat, tell Cline: "Create a workflow for the process I just completed." Cline analyzes the conversation, identifies the steps, and generates the workflow file. Your accumulated context becomes reusable automation.
|
||||
</Tip>
|
||||
|
||||
### Invoking Workflows
|
||||
|
||||
Type `/` in the chat input to see available workflows. Cline shows autocomplete suggestions as you type, so `/rel` would match `release-prep.md`. Select a workflow and press Enter to start it.
|
||||
|
||||
Cline executes each step in sequence, pausing for your approval when needed. You can stop a workflow at any point by rejecting a step.
|
||||
|
||||
### Toggling Workflows
|
||||
|
||||
Every workflow has a toggle to enable or disable it. This lets you control which workflows appear in the `/` menu without deleting the file.
|
||||
|
||||
## Where Workflows Live
|
||||
|
||||
Workflows can be stored in two locations: your project workspace or globally on your system.
|
||||
|
||||
**Workspace workflows** go in `.clinerules/workflows/` at your project root. Use these for project-specific automation like deployment scripts, release processes, or setup procedures that your team shares.
|
||||
|
||||
**Global workflows** go in your system's Cline Workflows directory. Use these for personal productivity workflows you use across all projects.
|
||||
|
||||
### Global Workflows Directory
|
||||
|
||||
| Operating System | Default Location |
|
||||
|------------------|------------------|
|
||||
| Windows | `Documents\Cline\Workflows` |
|
||||
| macOS | `~/Documents/Cline/Workflows` |
|
||||
| Linux/WSL | `~/Documents/Cline/Workflows` |
|
||||
|
||||
Workspace workflows take precedence when names match global workflows. See [Storage Locations](/customization/overview#storage-locations) for more guidance.
|
||||
|
||||
## What Workflows Can Use
|
||||
|
||||
Workflows can combine natural language instructions with specific tool calls. This flexibility lets you write workflows that are as simple or as precise as your task requires.
|
||||
|
||||
### Natural Language
|
||||
|
||||
Write steps as plain instructions. Cline interprets them and figures out which tools to use:
|
||||
|
||||
```markdown
|
||||
## Step 1: Check for uncommitted changes
|
||||
Look at the git status. If there are uncommitted changes, ask whether to continue or abort.
|
||||
|
||||
## Step 2: Run the test suite
|
||||
Execute all tests. If any fail, show the failures and stop.
|
||||
```
|
||||
|
||||
This approach works well when you want Cline to adapt to the situation rather than follow rigid steps.
|
||||
|
||||
### Cline Tools
|
||||
|
||||
For precise control, use Cline's built-in tools with XML syntax. This guarantees specific actions:
|
||||
|
||||
```xml
|
||||
<execute_command>
|
||||
<command>npm run test</command>
|
||||
<requires_approval>false</requires_approval>
|
||||
</execute_command>
|
||||
```
|
||||
|
||||
```xml
|
||||
<read_file>
|
||||
<path>src/config.json</path>
|
||||
</read_file>
|
||||
```
|
||||
|
||||
```xml
|
||||
<ask_followup_question>
|
||||
<question>Deploy to production or staging?</question>
|
||||
<options>["Production", "Staging", "Cancel"]</options>
|
||||
</ask_followup_question>
|
||||
```
|
||||
|
||||
See the full list in the [Cline Tools Reference](/tools-reference/all-cline-tools).
|
||||
|
||||
### CLI Tools
|
||||
|
||||
Reference any command-line tool installed on your machine. Git, npm, docker, gh, make, curl: whatever you have available.
|
||||
|
||||
```bash
|
||||
git log --author="$(git config user.name)" --since="yesterday" --oneline
|
||||
```
|
||||
|
||||
### MCP Tools
|
||||
|
||||
If you have [MCP servers](/mcp/mcp-overview) connected, use them in your workflows with the `use_mcp_tool` syntax. This lets you integrate with external services like GitHub, Slack, databases, or custom internal tools.
|
||||
|
||||
```xml
|
||||
<use_mcp_tool>
|
||||
<server_name>github-server</server_name>
|
||||
<tool_name>create_release</tool_name>
|
||||
<arguments>{"tag": "v1.2.0", "name": "Release v1.2.0", "body": "Changelog content here"}</arguments>
|
||||
</use_mcp_tool>
|
||||
```
|
||||
|
||||
Or describe the intent in natural language and let Cline figure out the tool call:
|
||||
|
||||
```markdown
|
||||
## Step 3: Create GitHub release
|
||||
Use the GitHub MCP server to create a release tagged with the version from package.json.
|
||||
Include the changelog as the release body.
|
||||
```
|
||||
|
||||
## Writing Effective Workflows
|
||||
|
||||
**Start simple.** Write natural language steps first. Only add XML tool calls when you need guaranteed behavior.
|
||||
|
||||
**Be specific about decisions.** If a step requires user input, make that explicit: "Ask whether to deploy to production or staging."
|
||||
|
||||
**Include failure handling.** Tell Cline what to do when something goes wrong: "If tests fail, show the failures and stop the workflow."
|
||||
|
||||
**Keep workflows focused.** A `deploy.md` should deploy. A `setup-db.md` should set up the database. Split complex processes into multiple workflows that can be run independently.
|
||||
|
||||
**Version control your workflows.** Store workflows in `.clinerules/workflows/` and commit them. Your team can share, review, and improve them together.
|
||||
|
||||
<Warning>
|
||||
Workflows execute with your permissions. Review workflows before running them, especially those from external sources.
|
||||
</Warning>
|
||||
|
||||
## Example: Release Preparation
|
||||
|
||||
This workflow automates the tedious pre-release checklist. It verifies your working directory is clean, runs tests and builds, prompts you for the version bump, and generates a changelog from recent commits.
|
||||
|
||||
The workflow demonstrates both approaches: XML tool syntax (`<execute_command>`, `<ask_followup_question>`) for steps that need precise control, and natural language for steps where Cline should adapt to the situation.
|
||||
|
||||
````markdown title="release-prep.md"
|
||||
# Release Preparation
|
||||
|
||||
Prepare a new release by running tests, building, and updating version info.
|
||||
|
||||
## Step 1: Check for clean working directory
|
||||
<execute_command>
|
||||
<command>git status --porcelain</command>
|
||||
</execute_command>
|
||||
|
||||
If there are uncommitted changes, ask whether to continue or stash them first.
|
||||
|
||||
## Step 2: Run the test suite
|
||||
<execute_command>
|
||||
<command>npm run test</command>
|
||||
</execute_command>
|
||||
|
||||
If any tests fail, stop the workflow and report the failures.
|
||||
|
||||
## Step 3: Build the project
|
||||
<execute_command>
|
||||
<command>npm run build</command>
|
||||
</execute_command>
|
||||
|
||||
Verify the build completes without errors.
|
||||
|
||||
## Step 4: Ask for new version
|
||||
<ask_followup_question>
|
||||
<question>What should the new version be?</question>
|
||||
<options>["Patch (x.x.X)", "Minor (x.X.0)", "Major (X.0.0)", "Custom"]</options>
|
||||
</ask_followup_question>
|
||||
|
||||
## Step 5: Update version
|
||||
Update the version in `package.json` to the new version specified by the user.
|
||||
|
||||
## Step 6: Generate changelog entry
|
||||
<execute_command>
|
||||
<command>git log --oneline $(git describe --tags --abbrev=0)..HEAD</command>
|
||||
</execute_command>
|
||||
|
||||
Use these commits to write a changelog entry for the new version.
|
||||
````
|
||||
|
||||
Invoke it with `/release-prep.md` and Cline walks through each step.
|
||||
@@ -91,7 +91,6 @@
|
||||
"customization/overview",
|
||||
"customization/cline-rules",
|
||||
"customization/skills",
|
||||
"customization/workflows",
|
||||
"customization/hooks",
|
||||
"customization/clineignore"
|
||||
]
|
||||
@@ -567,18 +566,6 @@
|
||||
"source": "/features/slash-commands/new-task",
|
||||
"destination": "/core-workflows/using-commands"
|
||||
},
|
||||
{
|
||||
"source": "/features/slash-commands/workflows/index",
|
||||
"destination": "/customization/workflows"
|
||||
},
|
||||
{
|
||||
"source": "/features/slash-commands/workflows/quickstart",
|
||||
"destination": "/customization/workflows"
|
||||
},
|
||||
{
|
||||
"source": "/features/slash-commands/workflows/best-practices",
|
||||
"destination": "/customization/workflows"
|
||||
},
|
||||
{
|
||||
"source": "/exploring-clines-tools/cline-tools-guide",
|
||||
"destination": "/tools-reference/all-cline-tools"
|
||||
|
||||
@@ -111,7 +111,7 @@ For each workspace folder, Cline detects:
|
||||
This means Cline understands that your frontend and backend might be at different commits, on different branches, or even use different version control systems.
|
||||
|
||||
<Note>
|
||||
While Cline detects VCS information for all workspace folders, certain features only use the **primary workspace** (the first folder): [Cline rules](/customization/cline-rules), [workflows](/customization/workflows), and [Git-related features](/core-workflows/working-with-files) like `@git` mentions.
|
||||
While Cline detects VCS information for all workspace folders, certain features only use the **primary workspace** (the first folder): [Cline rules](/customization/cline-rules) and [Git-related features](/core-workflows/working-with-files) like `@git` mentions.
|
||||
</Note>
|
||||
|
||||
## Referencing Files Across Workspaces
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ Cline is an AI coding agent that lives in your editor and your terminal. It can
|
||||
Learn the daily patterns: task management, plan & act, working with files, commands, and checkpoints.
|
||||
</Card>
|
||||
<Card title="Customization" icon="sliders" href="/customization/overview">
|
||||
Tailor Cline to your workflow with rules, skills, workflows, hooks, and .clineignore.
|
||||
Tailor Cline to your workflow with rules, skills, hooks, and .clineignore.
|
||||
</Card>
|
||||
<Card title="Features" icon="sparkles" href="/features/memory-bank">
|
||||
Discover Memory Bank, Focus Chain, auto-approve, subagents, Jupyter support, and more.
|
||||
|
||||
+3
-14
@@ -52,15 +52,12 @@ service FileService {
|
||||
// Toggle an Agents rule (enable or disable)
|
||||
rpc toggleAgentsRule(ToggleAgentsRuleRequest) returns (ClineRulesToggles);
|
||||
|
||||
// Refreshes all rule toggles (Cline, External, and Workflows)
|
||||
// Refreshes all rule toggles (Cline and external rule families)
|
||||
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
|
||||
|
||||
// Opens a task's conversation history file on disk
|
||||
rpc openDiskConversationHistory(StringRequest) returns (Empty);
|
||||
|
||||
// Toggles a workflow on or off
|
||||
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
|
||||
|
||||
// Check if file exists in the project
|
||||
rpc ifFileExistsRelativePath(StringRequest) returns (BooleanResponse);
|
||||
|
||||
@@ -102,8 +99,8 @@ message RefreshedRules {
|
||||
ClineRulesToggles local_cursor_rules_toggles = 3;
|
||||
ClineRulesToggles local_windsurf_rules_toggles = 4;
|
||||
ClineRulesToggles local_agents_rules_toggles = 5;
|
||||
ClineRulesToggles local_workflow_toggles = 6;
|
||||
ClineRulesToggles global_workflow_toggles = 7;
|
||||
reserved 6; // was local_workflow_toggles (workflows removed)
|
||||
reserved 7; // was global_workflow_toggles (workflows removed)
|
||||
}
|
||||
|
||||
// Request to toggle a Windsurf rule
|
||||
@@ -225,14 +222,6 @@ message ToggleCursorRuleRequest {
|
||||
bool enabled = 3; // Whether to enable or disable the rule
|
||||
}
|
||||
|
||||
// Request to toggle a workflow on or off
|
||||
message ToggleWorkflowRequest {
|
||||
Metadata metadata = 1;
|
||||
string workflow_path = 2;
|
||||
bool enabled = 3;
|
||||
RuleScope scope = 4; // Scope of the workflow (local, global, or remote)
|
||||
}
|
||||
|
||||
// Maps from hook name to enabled/disabled status
|
||||
message HookInfo {
|
||||
string name = 1;
|
||||
|
||||
@@ -240,7 +240,7 @@ message Settings {
|
||||
optional string lm_studio_model_id = 127;
|
||||
optional AutoApprovalSettings auto_approval_settings = 128;
|
||||
optional string global_cline_rules_toggles = 129;
|
||||
optional string global_workflow_toggles = 130;
|
||||
reserved 130; // was global_workflow_toggles (workflows removed)
|
||||
optional string global_skills_toggles = 131;
|
||||
optional BrowserSettings browser_settings = 132;
|
||||
optional string telemetry_setting = 133;
|
||||
|
||||
@@ -92,7 +92,6 @@ export const getLocalClineRules = async (
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "hooks"],
|
||||
[".clinerules", "skills"],
|
||||
])
|
||||
@@ -163,7 +162,6 @@ export async function refreshClineRulesToggles(
|
||||
const localClineRulesToggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "hooks"],
|
||||
[".clinerules", "skills"],
|
||||
])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ensureRulesDirectoryExists, ensureWorkflowsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { GlobalInstructionsFile } from "@shared/remote-config/schema"
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
@@ -42,7 +42,7 @@ export async function readDirectoryRecursive(
|
||||
export async function synchronizeRuleToggles(
|
||||
rulesDirectoryPath: string,
|
||||
currentToggles: ClineRulesToggles,
|
||||
allowedFileExtension: string = "",
|
||||
allowedFileExtension = "",
|
||||
excludedPaths: string[][] = [],
|
||||
): Promise<ClineRulesToggles> {
|
||||
// Create a copy of toggles to modify
|
||||
@@ -275,8 +275,8 @@ export function getRemoteRulesTotalContentWithMetadata(
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles converting any directory into a file (specifically used for .clinerules and .clinerules/workflows)
|
||||
* The old .clinerules file or .clinerules/workflows file will be renamed to a default filename
|
||||
* Handles converting a legacy .clinerules file path into a directory-backed rules layout.
|
||||
* The old .clinerules file will be renamed to a default filename inside the new directory.
|
||||
* Doesn't do anything if the dir already exists or doesn't exist
|
||||
* Returns whether there are any uncaught errors
|
||||
*/
|
||||
@@ -318,13 +318,8 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
if (type === "workflow") {
|
||||
const globalClineWorkflowFilePath = await ensureWorkflowsDirectoryExists()
|
||||
filePath = path.join(globalClineWorkflowFilePath, filename)
|
||||
} else {
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
}
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
@@ -335,21 +330,8 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
|
||||
|
||||
await fs.mkdir(localClineRulesFilePath, { recursive: true })
|
||||
|
||||
if (type === "workflow") {
|
||||
const localWorkflowsFilePath = path.resolve(cwd, GlobalFileNames.workflows)
|
||||
|
||||
const hasError = await ensureLocalClineDirExists(localWorkflowsFilePath, "default-workflows.md")
|
||||
if (hasError === true) {
|
||||
return { filePath: null, fileExists: false }
|
||||
}
|
||||
|
||||
await fs.mkdir(localWorkflowsFilePath, { recursive: true })
|
||||
|
||||
filePath = path.join(localWorkflowsFilePath, filename)
|
||||
} else {
|
||||
// clinerules file creation
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
// clinerules file creation
|
||||
filePath = path.join(localClineRulesFilePath, filename)
|
||||
}
|
||||
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
@@ -393,21 +375,11 @@ export async function deleteRuleFile(
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
if (type === "workflow") {
|
||||
const toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
|
||||
} else {
|
||||
const toggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
|
||||
}
|
||||
const toggles = controller.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
if (type === "workflow") {
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
|
||||
} else if (type === "cursor") {
|
||||
if (type === "cursor") {
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.stateManager.setWorkspaceState("localCursorRulesToggles", toggles)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { ensureWorkflowsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import path from "path"
|
||||
import { Controller } from "@/core/controller"
|
||||
|
||||
/**
|
||||
* Refresh the workflow toggles
|
||||
*/
|
||||
export async function refreshWorkflowToggles(
|
||||
controller: Controller,
|
||||
workingDirectory: string,
|
||||
): Promise<{
|
||||
globalWorkflowToggles: ClineRulesToggles
|
||||
localWorkflowToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global workflows
|
||||
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
|
||||
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
|
||||
|
||||
const workflowRulesToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", updatedWorkflowToggles)
|
||||
|
||||
return {
|
||||
globalWorkflowToggles: updatedGlobalWorkflowToggles,
|
||||
localWorkflowToggles: updatedWorkflowToggles,
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { refreshClineRulesToggles } from "@core/context/instructions/user-instru
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { getWorkspaceBasename } from "@core/workspace"
|
||||
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -40,7 +39,7 @@ export async function createRuleFile(controller: Controller, request: RuleFileRe
|
||||
throw new Error("Failed to create file.")
|
||||
}
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
const fileTypeName = "rule"
|
||||
|
||||
if (fileExists) {
|
||||
const message = `${fileTypeName} file "${request.filename}" already exists.`
|
||||
@@ -51,11 +50,7 @@ export async function createRuleFile(controller: Controller, request: RuleFileRe
|
||||
// Still open it for editing
|
||||
await openFile(controller, { value: filePath })
|
||||
} else {
|
||||
if (request.type === "workflow") {
|
||||
await refreshWorkflowToggles(controller, cwd)
|
||||
} else {
|
||||
await refreshClineRulesToggles(controller, cwd)
|
||||
}
|
||||
await refreshClineRulesToggles(controller, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await openFile(controller, { value: filePath })
|
||||
|
||||
@@ -38,12 +38,11 @@ export async function deleteRuleFile(controller: Controller, request: RuleFileRe
|
||||
// we refresh inside of the deleteRuleFileImpl(..) call
|
||||
//await refreshClineRulesToggles(controller.context, cwd)
|
||||
//await refreshExternalRulesToggles(controller.context, cwd)
|
||||
//await refreshWorkflowToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const fileName = getWorkspaceBasename(request.rulePath, "Controller.deleteRuleFile")
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
const fileTypeName = "rule"
|
||||
|
||||
const message = `${fileTypeName} file "${fileName}" deleted successfully`
|
||||
HostProvider.window.showMessage({
|
||||
|
||||
@@ -11,9 +11,8 @@ import { Controller } from ".."
|
||||
* Opens a file in the editor
|
||||
* @param controller The controller instance
|
||||
* @param request The request message containing the file path in the 'value' field.
|
||||
* Supports special URI format for remote rules/workflows:
|
||||
* Supports special URI format for remote rules:
|
||||
* - remote://rule/{ruleName}
|
||||
* - remote://workflow/{workflowName}
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function openFile(_controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
@@ -29,35 +28,30 @@ export async function openFile(_controller: Controller, request: StringRequest):
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a remote rule or workflow file by creating a temp file with its contents
|
||||
* @param uri The remote URI in format: remote://rule/{name} or remote://workflow/{name}
|
||||
* Opens a remote rule file by creating a temp file with its contents
|
||||
* @param uri The remote URI in format: remote://rule/{name}
|
||||
*/
|
||||
async function openRemoteFile(uri: string): Promise<void> {
|
||||
// Parse: remote://rule/{name} or remote://workflow/{name}
|
||||
const match = uri.match(/^remote:\/\/(rule|workflow)\/(.+)$/)
|
||||
const match = uri.match(/^remote:\/\/rule\/(.+)$/)
|
||||
if (!match) {
|
||||
throw new Error(`Invalid remote file URI: ${uri}`)
|
||||
}
|
||||
|
||||
const [, type, name] = match
|
||||
const [, name] = match
|
||||
const remoteConfig = StateManager.get().getRemoteConfigSettings()
|
||||
|
||||
// Look up content based on type
|
||||
const items = type === "rule" ? remoteConfig.remoteGlobalRules : remoteConfig.remoteGlobalWorkflows
|
||||
const item = items?.find((r) => r.name === name)
|
||||
const item = remoteConfig.remoteGlobalRules?.find((r) => r.name === name)
|
||||
|
||||
if (!item?.contents) {
|
||||
throw new Error(`Remote ${type} not found: ${name}`)
|
||||
throw new Error(`Remote rule not found: ${name}`)
|
||||
}
|
||||
|
||||
// Create temp file with read-only header comment
|
||||
const typeLabel = type === "rule" ? "rule" : "workflow"
|
||||
const header = `# ⚠️ READ-ONLY: This ${typeLabel} is managed by your organization.\n# Changes made here will not be saved.\n\n`
|
||||
const header = "# ⚠️ READ-ONLY: This rule is managed by your organization.\n# Changes made here will not be saved.\n\n"
|
||||
const content = header + item.contents
|
||||
|
||||
// Sanitize the name for use in filename (replace invalid characters)
|
||||
const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "_")
|
||||
const tempPath = path.join(os.tmpdir(), `cline-remote-${type}-${sanitizedName}.md`)
|
||||
const tempPath = path.join(os.tmpdir(), `cline-remote-rule-${sanitizedName}.md`)
|
||||
|
||||
await writeFile(tempPath, content)
|
||||
await openFileIntegration(tempPath)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { RefreshedRules } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
@@ -8,10 +7,10 @@ import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Refreshes all rule toggles (Cline, External, and Workflows)
|
||||
* Refreshes all rule toggles (Cline and external rule families)
|
||||
* @param controller The controller instance
|
||||
* @param _request The empty request
|
||||
* @returns RefreshedRules containing updated toggles for all rule types
|
||||
* @returns RefreshedRules containing updated toggles for supported rule types
|
||||
*/
|
||||
export async function refreshRules(controller: Controller, _request: EmptyRequest): Promise<RefreshedRules> {
|
||||
try {
|
||||
@@ -21,7 +20,6 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
|
||||
controller,
|
||||
cwd,
|
||||
)
|
||||
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller, cwd)
|
||||
|
||||
return RefreshedRules.create({
|
||||
globalClineRulesToggles: { toggles: globalToggles },
|
||||
@@ -29,8 +27,6 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
|
||||
localCursorRulesToggles: { toggles: cursorLocalToggles },
|
||||
localWindsurfRulesToggles: { toggles: windsurfLocalToggles },
|
||||
localAgentsRulesToggles: { toggles: agentsLocalToggles },
|
||||
localWorkflowToggles: { toggles: localWorkflowToggles },
|
||||
globalWorkflowToggles: { toggles: globalWorkflowToggles },
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error("Failed to refresh rules:", error)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ClineRulesToggles, RuleScope, ToggleWorkflowRequest } from "@shared/proto/cline/file"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Toggles a workflow on or off
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the workflow path and enabled state
|
||||
* @returns The updated workflow toggles
|
||||
*/
|
||||
export async function toggleWorkflow(controller: Controller, request: ToggleWorkflowRequest): Promise<ClineRulesToggles> {
|
||||
const { workflowPath, enabled, scope } = request
|
||||
|
||||
if (!workflowPath || typeof enabled !== "boolean" || scope === undefined) {
|
||||
Logger.error("toggleWorkflow: Missing or invalid parameters", {
|
||||
workflowPath,
|
||||
scope,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters for toggleWorkflow")
|
||||
}
|
||||
|
||||
// Handle the three different scopes
|
||||
let toggles: Record<string, boolean>
|
||||
|
||||
switch (scope) {
|
||||
case RuleScope.GLOBAL: {
|
||||
toggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
|
||||
break
|
||||
}
|
||||
case RuleScope.LOCAL: {
|
||||
toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
|
||||
break
|
||||
}
|
||||
case RuleScope.REMOTE: {
|
||||
toggles = controller.stateManager.getGlobalStateKey("remoteWorkflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.stateManager.setGlobalState("remoteWorkflowToggles", toggles)
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid scope: ${scope}`)
|
||||
}
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
// Return the updated toggles
|
||||
return ClineRulesToggles.create({ toggles: toggles })
|
||||
}
|
||||
@@ -864,11 +864,9 @@ export class Controller {
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting")
|
||||
const globalClineRulesToggles = this.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
const globalWorkflowToggles = this.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
const globalSkillsToggles = this.stateManager.getGlobalSettingsKey("globalSkillsToggles")
|
||||
const localSkillsToggles = this.stateManager.getWorkspaceStateKey("localSkillsToggles")
|
||||
const remoteRulesToggles = this.stateManager.getGlobalStateKey("remoteRulesToggles")
|
||||
const remoteWorkflowToggles = this.stateManager.getGlobalStateKey("remoteWorkflowToggles")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode")
|
||||
@@ -894,7 +892,6 @@ export class Controller {
|
||||
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
const localAgentsRulesToggles = this.stateManager.getWorkspaceStateKey("localAgentsRulesToggles")
|
||||
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
|
||||
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
|
||||
// Spread to create new array reference - React needs this to detect changes in useEffect dependencies
|
||||
@@ -950,12 +947,9 @@ export class Controller {
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
localAgentsRulesToggles: localAgentsRulesToggles || {},
|
||||
localWorkflowToggles: workflowToggles || {},
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
globalSkillsToggles: globalSkillsToggles || {},
|
||||
localSkillsToggles: localSkillsToggles || {},
|
||||
remoteRulesToggles: remoteRulesToggles,
|
||||
remoteWorkflowToggles: remoteWorkflowToggles,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
vscodeTerminalExecutionMode: vscodeTerminalExecutionMode,
|
||||
|
||||
@@ -21,68 +21,5 @@ export async function getAvailableSlashCommands(controller: Controller, _request
|
||||
)
|
||||
}
|
||||
|
||||
// Get workflow toggles from state
|
||||
const localWorkflowToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles") ?? {}
|
||||
const globalWorkflowToggles = controller.stateManager.getGlobalSettingsKey("globalWorkflowToggles") ?? {}
|
||||
const remoteWorkflowToggles = controller.stateManager.getGlobalStateKey("remoteWorkflowToggles") ?? {}
|
||||
const remoteConfigSettings = controller.stateManager.getRemoteConfigSettings()
|
||||
const remoteWorkflows = remoteConfigSettings?.remoteGlobalWorkflows ?? []
|
||||
|
||||
// Track local workflow names to avoid duplicates from global
|
||||
const localNames = new Set<string>()
|
||||
|
||||
// Add local workflows (enabled only)
|
||||
for (const [path, enabled] of Object.entries(localWorkflowToggles)) {
|
||||
if (enabled) {
|
||||
const fileName = fullPathToFileName(path)
|
||||
localNames.add(fileName)
|
||||
commands.push(
|
||||
SlashCommandInfo.create({
|
||||
name: fileName,
|
||||
description: `Custom workflow: ${fileName}`,
|
||||
section: "custom",
|
||||
cliCompatible: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add global workflows (enabled only, skip if local exists with same name)
|
||||
for (const [path, enabled] of Object.entries(globalWorkflowToggles)) {
|
||||
if (enabled) {
|
||||
const fileName = fullPathToFileName(path)
|
||||
if (!localNames.has(fileName)) {
|
||||
commands.push(
|
||||
SlashCommandInfo.create({
|
||||
name: fileName,
|
||||
description: `Custom workflow: ${fileName}`,
|
||||
section: "custom",
|
||||
cliCompatible: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add remote workflows that are enabled
|
||||
for (const workflow of remoteWorkflows) {
|
||||
const enabled = workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] !== false
|
||||
if (enabled) {
|
||||
commands.push(
|
||||
SlashCommandInfo.create({
|
||||
name: workflow.name,
|
||||
description: `Remote workflow: ${workflow.name}`,
|
||||
section: "custom",
|
||||
cliCompatible: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return SlashCommandsResponse.create({ commands })
|
||||
}
|
||||
|
||||
function fullPathToFileName(path: string): string {
|
||||
// e.g. replace /path/to/workflow.md with workflow.md
|
||||
return path.replace(/^.*[/\\]/, "")
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ describe("slash-commands", () => {
|
||||
|
||||
it("should process MCP prompt command in task tag", async () => {
|
||||
const text = "<task>/mcp:test-server:greet</task>"
|
||||
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
|
||||
const result = await parseSlashCommands(text, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
|
||||
|
||||
expect(result.processedText).to.include('<mcp_prompt server="test-server" prompt="greet">')
|
||||
expect(result.processedText).to.include("Hello from MCP!")
|
||||
@@ -114,7 +114,7 @@ describe("slash-commands", () => {
|
||||
|
||||
it("should process MCP prompt with additional text", async () => {
|
||||
const text = "<task>/mcp:test-server:greet Please expand on this</task>"
|
||||
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
|
||||
const result = await parseSlashCommands(text, "test-ulid", undefined, false, undefined, mockMcpPromptFetcher)
|
||||
|
||||
expect(result.processedText).to.include('<mcp_prompt server="test-server" prompt="greet">')
|
||||
expect(result.processedText).to.include("Please expand on this")
|
||||
@@ -131,14 +131,19 @@ describe("slash-commands", () => {
|
||||
}
|
||||
|
||||
const text = "<task>/mcp:server:prompt:with:colons</task>"
|
||||
const result = await parseSlashCommands(text, {}, {}, "test-ulid", undefined, false, undefined, fetcherWithColons)
|
||||
const result = await parseSlashCommands(text, "test-ulid", undefined, false, undefined, fetcherWithColons)
|
||||
|
||||
expect(result.processedText).to.include('prompt="prompt:with:colons"')
|
||||
expect(result.processedText).to.include("Colon prompt")
|
||||
})
|
||||
|
||||
// Note: Tests for "unknown MCP server", "no fetcher", and "fetcher errors"
|
||||
// are skipped because they require StateManager initialization when falling
|
||||
// through to workflow checking. The core MCP functionality is covered above.
|
||||
it("should treat arbitrary workflow-like filenames as unknown slash commands", async () => {
|
||||
const text = "<task>/release.md Ship the release</task>"
|
||||
const result = await parseSlashCommands(text, "test-ulid")
|
||||
|
||||
expect(result.processedText).to.equal(text)
|
||||
expect(result.processedText).to.not.include("<explicit_instructions")
|
||||
expect(result.needsClinerulesFileCheck).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { ApiProviderInfo } from "@core/api"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { McpPromptResponse } from "@shared/mcp"
|
||||
import fs from "fs/promises"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isNativeToolCallingConfig } from "@/utils/model-utils"
|
||||
@@ -13,36 +11,18 @@ import {
|
||||
newTaskToolResponse,
|
||||
reportBugToolResponse,
|
||||
} from "../prompts/commands"
|
||||
import { StateManager } from "../storage/StateManager"
|
||||
|
||||
/**
|
||||
* Callback type for fetching MCP prompts
|
||||
*/
|
||||
export type McpPromptFetcher = (serverName: string, promptName: string) => Promise<McpPromptResponse | null>
|
||||
|
||||
type FileBasedWorkflow = {
|
||||
fullPath: string
|
||||
fileName: string
|
||||
isRemote: false
|
||||
}
|
||||
|
||||
type RemoteWorkflow = {
|
||||
fullPath: string
|
||||
fileName: string
|
||||
isRemote: true
|
||||
contents: string
|
||||
}
|
||||
|
||||
type Workflow = FileBasedWorkflow | RemoteWorkflow
|
||||
|
||||
/**
|
||||
* Processes text for slash commands and transforms them with appropriate instructions
|
||||
* This is called after parseMentions() to process any slash commands in the user's message
|
||||
*/
|
||||
export async function parseSlashCommands(
|
||||
text: string,
|
||||
localWorkflowToggles: ClineRulesToggles,
|
||||
globalWorkflowToggles: ClineRulesToggles,
|
||||
ulid: string,
|
||||
focusChainSettings?: { enabled: boolean },
|
||||
enableNativeToolCalls?: boolean,
|
||||
@@ -165,78 +145,13 @@ export async function parseSlashCommands(
|
||||
|
||||
return { processedText, needsClinerulesFileCheck: false }
|
||||
}
|
||||
// Prompt not found - log for debugging and fall through to workflow checking
|
||||
// Prompt not found - log for debugging and fall through to default handling
|
||||
Logger.debug(`MCP prompt not found: ${commandName} (server: ${serverName}, prompt: ${promptName})`)
|
||||
} catch (error) {
|
||||
Logger.error(`Error fetching MCP prompt ${commandName}: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const globalWorkflows: Workflow[] = Object.entries(globalWorkflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => ({
|
||||
fullPath: filePath,
|
||||
fileName: filePath.replace(/^.*[/\\]/, ""),
|
||||
isRemote: false,
|
||||
}))
|
||||
|
||||
const localWorkflows: Workflow[] = Object.entries(localWorkflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([filePath, _]) => ({
|
||||
fullPath: filePath,
|
||||
fileName: filePath.replace(/^.*[/\\]/, ""),
|
||||
isRemote: false,
|
||||
}))
|
||||
|
||||
// Get remote workflows from remote config
|
||||
const stateManager = StateManager.get()
|
||||
const remoteConfigSettings = stateManager.getRemoteConfigSettings()
|
||||
const remoteWorkflows = remoteConfigSettings.remoteGlobalWorkflows || []
|
||||
const remoteWorkflowToggles = stateManager.getGlobalStateKey("remoteWorkflowToggles") || {}
|
||||
|
||||
const enabledRemoteWorkflows: Workflow[] = remoteWorkflows
|
||||
.filter((workflow) => {
|
||||
// If alwaysEnabled, always include; otherwise check toggle
|
||||
return workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] !== false
|
||||
})
|
||||
.map((workflow) => ({
|
||||
fullPath: "",
|
||||
fileName: workflow.name,
|
||||
isRemote: true,
|
||||
contents: workflow.contents,
|
||||
}))
|
||||
|
||||
// local workflows have precedence over global workflows, which have precedence over remote workflows
|
||||
const enabledWorkflows: Workflow[] = [...localWorkflows, ...globalWorkflows, ...enabledRemoteWorkflows]
|
||||
|
||||
// Then check if the command matches any enabled workflow filename
|
||||
const matchingWorkflow = enabledWorkflows.find((workflow) => workflow.fileName === commandName)
|
||||
|
||||
if (matchingWorkflow) {
|
||||
try {
|
||||
// Get workflow content - either from file or from remote config
|
||||
let workflowContent: string
|
||||
if (matchingWorkflow.isRemote) {
|
||||
workflowContent = matchingWorkflow.contents.trim()
|
||||
} else {
|
||||
workflowContent = (await fs.readFile(matchingWorkflow.fullPath, "utf8")).trim()
|
||||
}
|
||||
|
||||
// remove the slash command and add custom instructions at the top of this message
|
||||
const textWithoutSlashCommand = removeSlashCommand(text, tagContent, contentStartIndex, slashMatch)
|
||||
const processedText =
|
||||
`<explicit_instructions type="${matchingWorkflow.fileName}">\n${workflowContent}\n</explicit_instructions>\n` +
|
||||
textWithoutSlashCommand
|
||||
|
||||
// Track telemetry for workflow command usage
|
||||
telemetryService.captureSlashCommandUsed(ulid, commandName, "workflow")
|
||||
|
||||
return { processedText, needsClinerulesFileCheck: false }
|
||||
} catch (error) {
|
||||
Logger.error(`Error reading workflow file ${matchingWorkflow.fullPath}: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ export const GlobalFileNames = {
|
||||
hicapModels: "hicap_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
hooksDir: ".clinerules/hooks",
|
||||
clineruleSkillsDir: ".clinerules/skills",
|
||||
clineSkillsDir: ".cline/skills",
|
||||
@@ -133,17 +132,6 @@ export async function ensureRulesDirectoryExists(): Promise<string> {
|
||||
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> {
|
||||
const userDocumentsPath = await getDocumentsPath()
|
||||
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
|
||||
|
||||
@@ -217,13 +217,10 @@ export function transformRemoteConfigToStateShape(remoteConfig: RemoteConfig): P
|
||||
transformed.remoteConfiguredProviders = providers
|
||||
}
|
||||
|
||||
// Map global rules and workflows
|
||||
// Map global rules
|
||||
if (remoteConfig.globalRules !== undefined) {
|
||||
transformed.remoteGlobalRules = remoteConfig.globalRules
|
||||
}
|
||||
if (remoteConfig.globalWorkflows !== undefined) {
|
||||
transformed.remoteGlobalWorkflows = remoteConfig.globalWorkflows
|
||||
}
|
||||
|
||||
if (remoteConfig.enterpriseTelemetry?.promptUploading) {
|
||||
const promptUplaoding = remoteConfig.enterpriseTelemetry.promptUploading
|
||||
@@ -279,7 +276,6 @@ export function clearRemoteConfig() {
|
||||
telemetryService.removeProvider(REMOTE_CONFIG_OTEL_PROVIDER_ID)
|
||||
// the remote config cline rules toggle state is stored in global state
|
||||
stateManager.setGlobalState("remoteRulesToggles", {})
|
||||
stateManager.setGlobalState("remoteWorkflowToggles", {})
|
||||
|
||||
// clear secrets
|
||||
stateManager.setSecret("remoteLiteLlmApiKey", undefined)
|
||||
@@ -315,13 +311,10 @@ export async function applyRemoteConfig(
|
||||
|
||||
// Synchronize toggle state
|
||||
const currentRuleToggles = stateManager.getGlobalStateKey("remoteRulesToggles") || {}
|
||||
const currentWorkflowToggles = stateManager.getGlobalStateKey("remoteWorkflowToggles") || {}
|
||||
|
||||
const syncedRuleToggles = synchronizeRemoteRuleToggles(remoteConfig.globalRules || [], currentRuleToggles)
|
||||
const syncedWorkflowToggles = synchronizeRemoteRuleToggles(remoteConfig.globalWorkflows || [], currentWorkflowToggles)
|
||||
|
||||
stateManager.setGlobalState("remoteRulesToggles", syncedRuleToggles)
|
||||
stateManager.setGlobalState("remoteWorkflowToggles", syncedWorkflowToggles)
|
||||
|
||||
// Clear existing remote config cache
|
||||
stateManager.clearRemoteConfig()
|
||||
|
||||
@@ -110,7 +110,6 @@ import { Session } from "@/shared/services/Session"
|
||||
import { RuleContextBuilder } from "../context/instructions/user-instructions/RuleContextBuilder"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
import { discoverSkills, getAvailableSkills } from "../context/instructions/user-instructions/skills"
|
||||
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
|
||||
import { Controller } from "../controller"
|
||||
import { executeHook } from "../hooks/hook-executor"
|
||||
import { StateManager } from "../storage/StateManager"
|
||||
@@ -3323,7 +3322,6 @@ export class Task {
|
||||
const useNativeToolCalls = this.stateManager.getGlobalStateKey("nativeToolCallEnabled")
|
||||
const providerInfo = this.getCurrentProviderInfo()
|
||||
const cwd = this.cwd
|
||||
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.controller, cwd)
|
||||
|
||||
const hasUserContentTag = (text: string): boolean => {
|
||||
return USER_CONTENT_TAGS.some((tag) => text.includes(tag))
|
||||
@@ -3349,8 +3347,6 @@ export class Task {
|
||||
|
||||
const { processedText, needsClinerulesFileCheck: needsCheck } = await parseSlashCommands(
|
||||
parsedText,
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
ulid,
|
||||
focusChainSettings,
|
||||
useNativeToolCalls,
|
||||
|
||||
@@ -73,12 +73,9 @@ export interface ExtensionState {
|
||||
distinctId: string
|
||||
globalClineRulesToggles: ClineRulesToggles
|
||||
localClineRulesToggles: ClineRulesToggles
|
||||
localWorkflowToggles: ClineRulesToggles
|
||||
globalWorkflowToggles: ClineRulesToggles
|
||||
localCursorRulesToggles: ClineRulesToggles
|
||||
localWindsurfRulesToggles: ClineRulesToggles
|
||||
remoteRulesToggles?: ClineRulesToggles
|
||||
remoteWorkflowToggles?: ClineRulesToggles
|
||||
localAgentsRulesToggles: ClineRulesToggles
|
||||
mcpResponsesCollapsed?: boolean
|
||||
strictPlanModeEnabled?: boolean
|
||||
|
||||
@@ -672,13 +672,6 @@ describe("Remote Config Schema", () => {
|
||||
contents: "# Optional Guidelines\n\nConsider these best practices...",
|
||||
},
|
||||
],
|
||||
globalWorkflows: [
|
||||
{
|
||||
alwaysEnabled: true,
|
||||
name: "deployment-workflow.md",
|
||||
contents: "# Deployment Workflow\n\n1. Run tests\n2. Build\n3. Deploy",
|
||||
},
|
||||
],
|
||||
providerSettings: {
|
||||
OpenAiCompatible: {
|
||||
models: [
|
||||
@@ -836,11 +829,6 @@ describe("Remote Config Schema", () => {
|
||||
expect(result.globalRules?.[1].alwaysEnabled).to.equal(false)
|
||||
expect(result.globalRules?.[1].name).to.equal("optional-guidelines.md")
|
||||
|
||||
expect(result.globalWorkflows).to.have.lengthOf(1)
|
||||
expect(result.globalWorkflows?.[0].alwaysEnabled).to.equal(true)
|
||||
expect(result.globalWorkflows?.[0].name).to.equal("deployment-workflow.md")
|
||||
expect(result.globalWorkflows?.[0].contents).to.include("Deployment Workflow")
|
||||
|
||||
expect(result.enterpriseTelemetry?.promptUploading?.enabled).to.equal(true)
|
||||
expect(result.enterpriseTelemetry?.promptUploading?.type).to.equal("s3_access_keys")
|
||||
expect(result.enterpriseTelemetry?.promptUploading?.s3AccessSettings?.bucket).to.equal("enterprise-prompts")
|
||||
|
||||
@@ -136,13 +136,13 @@ export const RemoteMCPServerSchema = z.object({
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
// Settings for a global cline rules or workflow file.
|
||||
// Settings for a global cline rules file.
|
||||
export const GlobalInstructionsFileSchema = z.object({
|
||||
// When this is enabled, the user cannot turn off this rule or workflow.
|
||||
// When this is enabled, the user cannot turn off this rule.
|
||||
alwaysEnabled: z.boolean(),
|
||||
// The name of the rules or workflow file.
|
||||
// The name of the rule file.
|
||||
name: z.string(),
|
||||
// The contents of the rules or workflow file
|
||||
// The contents of the rule file
|
||||
contents: z.string(),
|
||||
})
|
||||
|
||||
@@ -221,9 +221,8 @@ export const RemoteConfigSchema = z.object({
|
||||
|
||||
enterpriseTelemetry: EnterpriseTelemetrySchema.optional(),
|
||||
|
||||
// Rules & Workflows
|
||||
// Rules
|
||||
globalRules: z.array(GlobalInstructionsFileSchema).optional(),
|
||||
globalWorkflows: z.array(GlobalInstructionsFileSchema).optional(),
|
||||
})
|
||||
|
||||
export const APIKeySchema = z.record(z.string(), z.string())
|
||||
|
||||
@@ -54,7 +54,6 @@ const REMOTE_CONFIG_EXTRA_FIELDS = {
|
||||
remoteMCPServers: { default: undefined as Array<{ name: string; url: string; alwaysEnabled?: boolean }> | undefined },
|
||||
previousRemoteMCPServers: { default: undefined as Array<{ name: string; url: string }> | undefined },
|
||||
remoteGlobalRules: { default: undefined as GlobalInstructionsFile[] | undefined },
|
||||
remoteGlobalWorkflows: { default: undefined as GlobalInstructionsFile[] | undefined },
|
||||
blockPersonalRemoteMCPServers: { default: false as boolean },
|
||||
openTelemetryOtlpHeaders: { default: undefined as Record<string, string> | undefined },
|
||||
otlpMetricsHeaders: { default: undefined as Record<string, string> | undefined },
|
||||
@@ -88,7 +87,6 @@ const GLOBAL_STATE_FIELDS = {
|
||||
lastDismissedCliBannerVersion: { default: 0 as number },
|
||||
nativeToolCallEnabled: { default: true as boolean },
|
||||
remoteRulesToggles: { default: {} as ClineRulesToggles },
|
||||
remoteWorkflowToggles: { default: {} as ClineRulesToggles },
|
||||
dismissedBanners: { default: [] as Array<{ bannerId: string; dismissedAt: number }> },
|
||||
// Path to worktree that should auto-open Cline sidebar when launched
|
||||
worktreeAutoOpenPath: { default: undefined as string | undefined },
|
||||
@@ -245,7 +243,6 @@ const USER_SETTINGS_FIELDS = {
|
||||
default: DEFAULT_AUTO_APPROVAL_SETTINGS as AutoApprovalSettings,
|
||||
},
|
||||
globalClineRulesToggles: { default: {} as ClineRulesToggles },
|
||||
globalWorkflowToggles: { default: {} as ClineRulesToggles },
|
||||
globalSkillsToggles: { default: {} as Record<string, boolean> },
|
||||
browserSettings: {
|
||||
default: DEFAULT_BROWSER_SETTINGS as BrowserSettings,
|
||||
@@ -359,7 +356,6 @@ export const LocalStateKeys = [
|
||||
"localWindsurfRulesToggles",
|
||||
"localAgentsRulesToggles",
|
||||
"localSkillsToggles",
|
||||
"workflowToggles",
|
||||
] as const
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -12,29 +12,10 @@ import { BASE_SLASH_COMMANDS } from "../shared/slashCommands"
|
||||
*/
|
||||
describe("getAvailableSlashCommands", () => {
|
||||
let mockController: Partial<Controller>
|
||||
let mockStateManager: {
|
||||
getWorkspaceStateKey: sinon.SinonStub
|
||||
getGlobalSettingsKey: sinon.SinonStub
|
||||
getGlobalStateKey: sinon.SinonStub
|
||||
getRemoteConfigSettings: sinon.SinonStub
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockStateManager = {
|
||||
getWorkspaceStateKey: sinon.stub(),
|
||||
getGlobalSettingsKey: sinon.stub(),
|
||||
getGlobalStateKey: sinon.stub(),
|
||||
getRemoteConfigSettings: sinon.stub(),
|
||||
}
|
||||
|
||||
// Default stubs return empty/null values
|
||||
mockStateManager.getWorkspaceStateKey.returns(null)
|
||||
mockStateManager.getGlobalSettingsKey.returns(null)
|
||||
mockStateManager.getGlobalStateKey.returns(null)
|
||||
mockStateManager.getRemoteConfigSettings.returns(null)
|
||||
|
||||
mockController = {
|
||||
stateManager: mockStateManager as any,
|
||||
stateManager: {} as any,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,209 +58,20 @@ describe("getAvailableSlashCommands", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Local Workflow Toggles", () => {
|
||||
it("should include enabled local workflows", async () => {
|
||||
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
|
||||
"/path/to/my-workflow.md": true,
|
||||
"/path/to/another-workflow.md": true,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const myWorkflow = response.commands.find((cmd) => cmd.name === "my-workflow.md")
|
||||
myWorkflow!.should.not.be.undefined()
|
||||
myWorkflow!.section.should.equal("custom")
|
||||
myWorkflow!.cliCompatible.should.equal(true)
|
||||
|
||||
const anotherWorkflow = response.commands.find((cmd) => cmd.name === "another-workflow.md")
|
||||
anotherWorkflow!.should.not.be.undefined()
|
||||
})
|
||||
|
||||
it("should exclude disabled local workflows", async () => {
|
||||
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
|
||||
"/path/to/enabled-workflow.md": true,
|
||||
"/path/to/disabled-workflow.md": false,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const enabled = response.commands.find((cmd) => cmd.name === "enabled-workflow.md")
|
||||
enabled!.should.not.be.undefined()
|
||||
|
||||
const disabled = response.commands.find((cmd) => cmd.name === "disabled-workflow.md")
|
||||
;(disabled === undefined).should.be.true()
|
||||
})
|
||||
|
||||
it("should extract filename from full path", async () => {
|
||||
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
|
||||
"/Users/test/project/.clinerules/workflows/deep-analysis.md": true,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "deep-analysis.md")
|
||||
workflow!.should.not.be.undefined()
|
||||
})
|
||||
|
||||
it("should handle Windows-style paths", async () => {
|
||||
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
|
||||
"C:\\Users\\test\\project\\.clinerules\\workflows\\windows-workflow.md": true,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "windows-workflow.md")
|
||||
workflow!.should.not.be.undefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Global Workflow Toggles", () => {
|
||||
it("should include enabled global workflows", async () => {
|
||||
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
|
||||
"/global/path/global-workflow.md": true,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "global-workflow.md")
|
||||
workflow!.should.not.be.undefined()
|
||||
workflow!.section.should.equal("custom")
|
||||
})
|
||||
|
||||
it("should exclude disabled global workflows", async () => {
|
||||
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
|
||||
"/global/path/disabled-global.md": false,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "disabled-global.md")
|
||||
;(workflow === undefined).should.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Workflow Deduplication", () => {
|
||||
it("should prefer local workflows over global workflows with same name", async () => {
|
||||
// Same filename in both local and global
|
||||
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
|
||||
"/local/path/shared-workflow.md": true,
|
||||
})
|
||||
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
|
||||
"/global/path/shared-workflow.md": true,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
// Should only appear once
|
||||
const matches = response.commands.filter((cmd) => cmd.name === "shared-workflow.md")
|
||||
matches.length.should.equal(1)
|
||||
})
|
||||
|
||||
it("should include global workflow if local with same name is disabled", async () => {
|
||||
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({
|
||||
"/local/path/shared-workflow.md": false, // disabled locally
|
||||
})
|
||||
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({
|
||||
"/global/path/shared-workflow.md": true, // enabled globally
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
// Global should appear since local is disabled
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "shared-workflow.md")
|
||||
workflow!.should.not.be.undefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Remote Workflows", () => {
|
||||
it("should include alwaysEnabled remote workflows", async () => {
|
||||
mockStateManager.getRemoteConfigSettings.returns({
|
||||
remoteGlobalWorkflows: [{ name: "always-on-workflow", alwaysEnabled: true }],
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "always-on-workflow")
|
||||
workflow!.should.not.be.undefined()
|
||||
workflow!.section.should.equal("custom")
|
||||
})
|
||||
|
||||
it("should include remote workflows enabled by toggle", async () => {
|
||||
mockStateManager.getRemoteConfigSettings.returns({
|
||||
remoteGlobalWorkflows: [{ name: "toggle-workflow", alwaysEnabled: false }],
|
||||
})
|
||||
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({
|
||||
"toggle-workflow": true, // not explicitly disabled
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "toggle-workflow")
|
||||
workflow!.should.not.be.undefined()
|
||||
})
|
||||
|
||||
it("should exclude remote workflows explicitly disabled by toggle", async () => {
|
||||
mockStateManager.getRemoteConfigSettings.returns({
|
||||
remoteGlobalWorkflows: [{ name: "disabled-remote", alwaysEnabled: false }],
|
||||
})
|
||||
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({
|
||||
"disabled-remote": false,
|
||||
})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "disabled-remote")
|
||||
;(workflow === undefined).should.be.true()
|
||||
})
|
||||
|
||||
it("should include remote workflows by default if not explicitly disabled", async () => {
|
||||
mockStateManager.getRemoteConfigSettings.returns({
|
||||
remoteGlobalWorkflows: [{ name: "default-enabled", alwaysEnabled: false }],
|
||||
})
|
||||
// No toggle entry for this workflow
|
||||
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
const workflow = response.commands.find((cmd) => cmd.name === "default-enabled")
|
||||
workflow!.should.not.be.undefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle null/undefined state values gracefully", async () => {
|
||||
mockStateManager.getWorkspaceStateKey.returns(null)
|
||||
mockStateManager.getGlobalSettingsKey.returns(undefined)
|
||||
mockStateManager.getGlobalStateKey.returns(null)
|
||||
mockStateManager.getRemoteConfigSettings.returns(null)
|
||||
|
||||
it("should only return base commands when feature-specific workflow state exists elsewhere", async () => {
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
// Should still return base commands
|
||||
response.commands.length.should.be.greaterThanOrEqual(BASE_SLASH_COMMANDS.length)
|
||||
})
|
||||
|
||||
it("should handle empty workflow toggle objects", async () => {
|
||||
mockStateManager.getWorkspaceStateKey.withArgs("workflowToggles").returns({})
|
||||
mockStateManager.getGlobalSettingsKey.withArgs("globalWorkflowToggles").returns({})
|
||||
mockStateManager.getGlobalStateKey.withArgs("remoteWorkflowToggles").returns({})
|
||||
mockStateManager.getRemoteConfigSettings.returns({
|
||||
remoteGlobalWorkflows: [],
|
||||
})
|
||||
|
||||
it("should not return any custom workflow commands", async () => {
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
// Should only have base commands
|
||||
const customCommands = response.commands.filter((cmd) => cmd.section === "custom")
|
||||
customCommands.should.have.length(0)
|
||||
response.commands.length.should.equal(BASE_SLASH_COMMANDS.length)
|
||||
})
|
||||
|
||||
it("should handle remote config with no remoteGlobalWorkflows property", async () => {
|
||||
mockStateManager.getRemoteConfigSettings.returns({})
|
||||
|
||||
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
|
||||
|
||||
// Should not throw, just return base commands
|
||||
response.commands.length.should.be.greaterThanOrEqual(BASE_SLASH_COMMANDS.length)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+24
-24
@@ -198,7 +198,7 @@ describe("Filesystem Utilities", () => {
|
||||
files.sort().should.deepEqual(expectedFiles.sort())
|
||||
})
|
||||
|
||||
it("should exclude .clinerules/workflows directory specifically", async () => {
|
||||
it("should exclude .clinerules/skills directory specifically", async () => {
|
||||
// Create a test directory structure
|
||||
const clinerulesDirTest = path.join(tmpDir, "clinerules-test")
|
||||
const clinerulesDirPath = path.join(clinerulesDirTest, ".clinerules")
|
||||
@@ -214,24 +214,24 @@ describe("Filesystem Utilities", () => {
|
||||
await fs.writeFile(path.join(otherDirPath, "helper.js"), "// helper code")
|
||||
await fs.writeFile(path.join(otherDirPath, "util.js"), "// util functions")
|
||||
|
||||
// Create .clinerules/workflows directory and files
|
||||
const workflowsDirPath = path.join(clinerulesDirPath, "workflows")
|
||||
await fs.mkdir(workflowsDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(workflowsDirPath, "workflow1.js"), "// workflow1")
|
||||
await fs.writeFile(path.join(workflowsDirPath, "workflow2.js"), "// workflow2")
|
||||
// Create .clinerules/skills directory and files
|
||||
const skillsDirPath = path.join(clinerulesDirPath, "skills")
|
||||
await fs.mkdir(skillsDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(skillsDirPath, "skill1.md"), "# skill1")
|
||||
await fs.writeFile(path.join(skillsDirPath, "skill2.md"), "# skill2")
|
||||
|
||||
// Get all files WITHOUT exclusion
|
||||
const allFiles = await readDirectory(clinerulesDirPath)
|
||||
|
||||
// Verify all files are included
|
||||
allFiles.length.should.equal(6) // 2 in root + 2 in other + 2 in workflows
|
||||
allFiles.some((file) => file.includes("workflow1.js")).should.be.true()
|
||||
allFiles.some((file) => file.includes("workflow2.js")).should.be.true()
|
||||
allFiles.length.should.equal(6) // 2 in root + 2 in other + 2 in skills
|
||||
allFiles.some((file) => file.includes("skill1.md")).should.be.true()
|
||||
allFiles.some((file) => file.includes("skill2.md")).should.be.true()
|
||||
|
||||
// Get files WITH workflows directory excluded
|
||||
const filteredFiles = await readDirectory(clinerulesDirPath, [[".clinerules", "workflows"]])
|
||||
// Get files WITH skills directory excluded
|
||||
const filteredFiles = await readDirectory(clinerulesDirPath, [[".clinerules", "skills"]])
|
||||
|
||||
// Verify workflows files are excluded but others remain
|
||||
// Verify skills files are excluded but others remain
|
||||
filteredFiles.length.should.equal(4) // 2 in root + 2 in other
|
||||
|
||||
const expectedFiles = [
|
||||
@@ -245,11 +245,11 @@ describe("Filesystem Utilities", () => {
|
||||
|
||||
// Test with multiple exclusions
|
||||
const multiExcludeFiles = await readDirectory(clinerulesDirPath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "skills"],
|
||||
[".clinerules", "other"],
|
||||
])
|
||||
|
||||
// Verify both workflows and other directories are excluded
|
||||
// Verify both skills and other directories are excluded
|
||||
multiExcludeFiles.length.should.equal(2) // only the 2 files in root
|
||||
|
||||
const rootOnlyFiles = [path.resolve(clinerulesDirPath, "config.json"), path.resolve(clinerulesDirPath, "settings.js")]
|
||||
@@ -267,10 +267,10 @@ describe("Filesystem Utilities", () => {
|
||||
await fs.writeFile(path.join(clinerulesDirPath, "config.json"), "{}")
|
||||
await fs.writeFile(path.join(clinerulesDirPath, "settings.js"), "// settings")
|
||||
|
||||
// Create .clinerules/workflows directory and files
|
||||
const workflowsDirPath = path.join(clinerulesDirPath, "workflows")
|
||||
await fs.mkdir(workflowsDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(workflowsDirPath, "workflow1.js"), "// workflow1")
|
||||
// Create .clinerules/skills directory and files
|
||||
const skillsDirPath = path.join(clinerulesDirPath, "skills")
|
||||
await fs.mkdir(skillsDirPath, { recursive: true })
|
||||
await fs.writeFile(path.join(skillsDirPath, "skill1.md"), "# skill1")
|
||||
|
||||
// Create .clinerules/hooks directory and files
|
||||
const hooksDirPath = path.join(clinerulesDirPath, "hooks")
|
||||
@@ -282,7 +282,7 @@ describe("Filesystem Utilities", () => {
|
||||
const allFiles = await readDirectory(clinerulesDirPath)
|
||||
|
||||
// Verify all files are included
|
||||
allFiles.length.should.equal(5) // 2 in root + 1 in workflows + 2 in hooks
|
||||
allFiles.length.should.equal(5) // 2 in root + 1 in skills + 2 in hooks
|
||||
allFiles.some((file) => file.includes("PreToolUse")).should.be.true()
|
||||
allFiles.some((file) => file.includes("PostToolUse")).should.be.true()
|
||||
|
||||
@@ -290,23 +290,23 @@ describe("Filesystem Utilities", () => {
|
||||
const filteredFiles = await readDirectory(clinerulesDirPath, [[".clinerules", "hooks"]])
|
||||
|
||||
// Verify hooks files are excluded but others remain
|
||||
filteredFiles.length.should.equal(3) // 2 in root + 1 in workflows
|
||||
filteredFiles.length.should.equal(3) // 2 in root + 1 in skills
|
||||
|
||||
const expectedFiles = [
|
||||
path.resolve(clinerulesDirPath, "config.json"),
|
||||
path.resolve(clinerulesDirPath, "settings.js"),
|
||||
path.resolve(workflowsDirPath, "workflow1.js"),
|
||||
path.resolve(skillsDirPath, "skill1.md"),
|
||||
]
|
||||
|
||||
filteredFiles.sort().should.deepEqual(expectedFiles.sort())
|
||||
|
||||
// Test with multiple exclusions (both workflows and hooks)
|
||||
// Test with multiple exclusions (both skills and hooks)
|
||||
const multiExcludeFiles = await readDirectory(clinerulesDirPath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "skills"],
|
||||
[".clinerules", "hooks"],
|
||||
])
|
||||
|
||||
// Verify both workflows and hooks directories are excluded
|
||||
// Verify both skills and hooks directories are excluded
|
||||
multiExcludeFiles.length.should.equal(2) // only the 2 files in root
|
||||
|
||||
const rootOnlyFiles = [path.resolve(clinerulesDirPath, "config.json"), path.resolve(clinerulesDirPath, "settings.js")]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"remoteRulesToggles": {},
|
||||
"remoteWorkflowToggles": {},
|
||||
"welcomeViewCompleted": true,
|
||||
"actModeOpenRouterModelInfo": {
|
||||
"name": "Anthropic: Claude Sonnet 4.6",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"remoteRulesToggles": {},
|
||||
"remoteWorkflowToggles": {},
|
||||
"actModeOpenRouterModelInfo": {
|
||||
"name": "Anthropic: Claude Sonnet 4.6",
|
||||
"maxTokens": 128000,
|
||||
@@ -56,7 +55,6 @@
|
||||
"enableNotifications": false
|
||||
},
|
||||
"primaryRootIndex": 0,
|
||||
"globalWorkflowToggles": {},
|
||||
"globalClineRulesToggles": {},
|
||||
"isNewUser": false
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"clineApiKey": null,
|
||||
"taskHistory": [],
|
||||
"remoteRulesToggles": {},
|
||||
"remoteWorkflowToggles": {},
|
||||
"actModeThinkingBudgetTokens": 0,
|
||||
"planModeThinkingBudgetTokens": 0,
|
||||
"welcomeViewCompleted": false
|
||||
|
||||
@@ -212,18 +212,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const {
|
||||
mode,
|
||||
apiConfiguration,
|
||||
openRouterModels,
|
||||
platform,
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings,
|
||||
navigateToSettingsModelPicker,
|
||||
mcpServers,
|
||||
} = useExtensionState()
|
||||
const { mode, apiConfiguration, openRouterModels, platform, navigateToSettingsModelPicker, mcpServers } =
|
||||
useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false)
|
||||
const [gitCommits, setGitCommits] = useState<GitCommit[]>([])
|
||||
@@ -471,15 +461,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
event.preventDefault()
|
||||
setSelectedSlashCommandsIndex((prevIndex) => {
|
||||
const direction = event.key === "ArrowUp" ? -1 : 1
|
||||
// Get commands with workflow toggles
|
||||
const allCommands = getMatchingSlashCommands(
|
||||
slashCommandsQuery,
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings?.remoteGlobalWorkflows,
|
||||
mcpServers,
|
||||
)
|
||||
const allCommands = getMatchingSlashCommands(slashCommandsQuery, mcpServers)
|
||||
|
||||
if (allCommands.length === 0) {
|
||||
return prevIndex
|
||||
@@ -497,14 +479,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
|
||||
if ((event.key === "Enter" || event.key === "Tab") && selectedSlashCommandsIndex !== -1) {
|
||||
event.preventDefault()
|
||||
const commands = getMatchingSlashCommands(
|
||||
slashCommandsQuery,
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings?.remoteGlobalWorkflows,
|
||||
mcpServers,
|
||||
)
|
||||
const commands = getMatchingSlashCommands(slashCommandsQuery, mcpServers)
|
||||
if (commands.length > 0) {
|
||||
handleSlashCommandsSelect(commands[selectedSlashCommandsIndex])
|
||||
}
|
||||
@@ -956,13 +931,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
|
||||
// Extract just the command name (without the slash)
|
||||
const commandName = command.substring(1)
|
||||
const isValidCommand = validateSlashCommand(
|
||||
commandName,
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteConfigSettings?.remoteGlobalWorkflows,
|
||||
)
|
||||
const isValidCommand = validateSlashCommand(commandName, mcpServers)
|
||||
|
||||
if (isValidCommand) {
|
||||
hasHighlightedSlashCommand = true
|
||||
@@ -975,7 +944,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
highlightLayerRef.current.innerHTML = processedText
|
||||
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
|
||||
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
|
||||
}, [localWorkflowToggles, globalWorkflowToggles, remoteWorkflowToggles, remoteConfigSettings])
|
||||
}, [mcpServers])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateHighlights()
|
||||
@@ -1363,14 +1332,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
{showSlashCommandsMenu && (
|
||||
<div ref={slashCommandsMenuContainerRef}>
|
||||
<SlashCommandMenu
|
||||
globalWorkflowToggles={globalWorkflowToggles}
|
||||
localWorkflowToggles={localWorkflowToggles}
|
||||
mcpServers={mcpServers}
|
||||
onMouseDown={handleMenuMouseDown}
|
||||
onSelect={handleSlashCommandsSelect}
|
||||
query={slashCommandsQuery}
|
||||
remoteWorkflows={remoteConfigSettings?.remoteGlobalWorkflows}
|
||||
remoteWorkflowToggles={remoteWorkflowToggles}
|
||||
selectedIndex={selectedSlashCommandsIndex}
|
||||
setSelectedIndex={setSelectedSlashCommandsIndex}
|
||||
/>
|
||||
@@ -1492,7 +1457,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
/>
|
||||
{!inputValue && selectedImages.length === 0 && selectedFiles.length === 0 && (
|
||||
<div className="text-xs absolute bottom-5 left-6.5 right-16 text-(--vscode-input-placeholderForeground)/50 whitespace-nowrap overflow-hidden text-ellipsis pointer-events-none z-1">
|
||||
Type @ for context, / for slash commands & workflows, hold shift to drag in files/images
|
||||
Type @ for context, / for slash commands, hold shift to drag in files/images
|
||||
</div>
|
||||
)}
|
||||
{(selectedImages.length > 0 || selectedFiles.length > 0) && (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { McpServer } from "@shared/mcp"
|
||||
import type { SlashCommand } from "@/utils/slash-commands"
|
||||
import React, { useCallback, useEffect, useRef } from "react"
|
||||
import ScreenReaderAnnounce from "@/components/common/ScreenReaderAnnounce"
|
||||
import { useMenuAnnouncement } from "@/hooks/useMenuAnnouncement"
|
||||
import type { SlashCommand } from "@/utils/slash-commands"
|
||||
import { getMatchingSlashCommands } from "@/utils/slash-commands"
|
||||
|
||||
interface SlashCommandMenuProps {
|
||||
@@ -11,10 +11,6 @@ interface SlashCommandMenuProps {
|
||||
setSelectedIndex: (index: number) => void
|
||||
onMouseDown: () => void
|
||||
query: string
|
||||
localWorkflowToggles?: Record<string, boolean>
|
||||
globalWorkflowToggles?: Record<string, boolean>
|
||||
remoteWorkflowToggles?: Record<string, boolean>
|
||||
remoteWorkflows?: any[]
|
||||
mcpServers?: McpServer[]
|
||||
}
|
||||
|
||||
@@ -24,25 +20,13 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||
setSelectedIndex,
|
||||
onMouseDown,
|
||||
query,
|
||||
localWorkflowToggles = {},
|
||||
globalWorkflowToggles = {},
|
||||
remoteWorkflowToggles,
|
||||
remoteWorkflows,
|
||||
mcpServers = [],
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Filter commands based on query
|
||||
const filteredCommands = getMatchingSlashCommands(
|
||||
query,
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteWorkflows,
|
||||
mcpServers,
|
||||
)
|
||||
const filteredCommands = getMatchingSlashCommands(query, mcpServers)
|
||||
const defaultCommands = filteredCommands.filter((cmd) => cmd.section === "default" || !cmd.section)
|
||||
const workflowCommands = filteredCommands.filter((cmd) => cmd.section === "custom")
|
||||
const mcpCommands = filteredCommands.filter((cmd) => cmd.section === "mcp")
|
||||
|
||||
// Screen reader announcements
|
||||
@@ -139,13 +123,7 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({
|
||||
{filteredCommands.length > 0 ? (
|
||||
<>
|
||||
{renderCommandSection(defaultCommands, "Default Commands", 0, true)}
|
||||
{renderCommandSection(workflowCommands, "Workflow Commands", defaultCommands.length, false)}
|
||||
{renderCommandSection(
|
||||
mcpCommands,
|
||||
"MCP Prompts",
|
||||
defaultCommands.length + workflowCommands.length,
|
||||
true,
|
||||
)}
|
||||
{renderCommandSection(mcpCommands, "MCP Prompts", defaultCommands.length, true)}
|
||||
</>
|
||||
) : (
|
||||
<div aria-selected="false" className="py-2 px-3 cursor-default flex flex-col" role="option">
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ToggleCursorRuleRequest,
|
||||
ToggleSkillRequest,
|
||||
ToggleWindsurfRuleRequest,
|
||||
ToggleWorkflowRequest,
|
||||
} from "@shared/proto/cline/file"
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
@@ -32,12 +31,9 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
localCursorRulesToggles = {},
|
||||
localWindsurfRulesToggles = {},
|
||||
localAgentsRulesToggles = {},
|
||||
localWorkflowToggles = {},
|
||||
globalWorkflowToggles = {},
|
||||
globalSkillsToggles = {},
|
||||
localSkillsToggles = {},
|
||||
remoteRulesToggles = {},
|
||||
remoteWorkflowToggles = {},
|
||||
remoteConfigSettings = {},
|
||||
hooksEnabled,
|
||||
setGlobalClineRulesToggles,
|
||||
@@ -45,12 +41,9 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
setLocalCursorRulesToggles,
|
||||
setLocalWindsurfRulesToggles,
|
||||
setLocalAgentsRulesToggles,
|
||||
setLocalWorkflowToggles,
|
||||
setGlobalWorkflowToggles,
|
||||
setGlobalSkillsToggles,
|
||||
setLocalSkillsToggles,
|
||||
setRemoteRulesToggles,
|
||||
setRemoteWorkflowToggles,
|
||||
} = useExtensionState()
|
||||
const [globalHooks, setGlobalHooks] = useState<Array<{ name: string; enabled: boolean; absolutePath: string }>>([])
|
||||
const [workspaceHooks, setWorkspaceHooks] = useState<
|
||||
@@ -66,7 +59,7 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const [arrowPosition, setArrowPosition] = useState(0)
|
||||
const [menuPosition, setMenuPosition] = useState(0)
|
||||
const [currentView, setCurrentView] = useState<"rules" | "workflows" | "hooks" | "skills">("rules")
|
||||
const [currentView, setCurrentView] = useState<"rules" | "hooks" | "skills">("rules")
|
||||
|
||||
// Auto-switch to rules tab if hooks become disabled while viewing hooks tab
|
||||
useEffect(() => {
|
||||
@@ -95,12 +88,6 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
if (response.localAgentsRulesToggles?.toggles) {
|
||||
setLocalAgentsRulesToggles(response.localAgentsRulesToggles.toggles)
|
||||
}
|
||||
if (response.localWorkflowToggles?.toggles) {
|
||||
setLocalWorkflowToggles(response.localWorkflowToggles.toggles)
|
||||
}
|
||||
if (response.globalWorkflowToggles?.toggles) {
|
||||
setGlobalWorkflowToggles(response.globalWorkflowToggles.toggles)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to refresh rules:", error)
|
||||
@@ -110,10 +97,8 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
isVisible,
|
||||
setGlobalClineRulesToggles,
|
||||
setLocalClineRulesToggles,
|
||||
setGlobalWorkflowToggles,
|
||||
setLocalCursorRulesToggles,
|
||||
setLocalWindsurfRulesToggles,
|
||||
setLocalWorkflowToggles,
|
||||
])
|
||||
|
||||
// Refresh hooks when hooks tab becomes visible
|
||||
@@ -213,21 +198,11 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
.map(([path, enabled]): [string, boolean] => [path, enabled as boolean])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
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])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
|
||||
// Get remote rules and workflows from remote config
|
||||
// Get remote rules from remote config
|
||||
const remoteGlobalRules = remoteConfigSettings.remoteGlobalRules || []
|
||||
const remoteGlobalWorkflows = remoteConfigSettings.remoteGlobalWorkflows || []
|
||||
|
||||
// Check if we have any remote rules or workflows
|
||||
// Check if we have any remote rules
|
||||
const hasRemoteRules = remoteGlobalRules.length > 0
|
||||
const hasRemoteWorkflows = remoteGlobalWorkflows.length > 0
|
||||
|
||||
// Handle toggle rule using gRPC
|
||||
const toggleRule = (isGlobal: boolean, rulePath: string, enabled: boolean) => {
|
||||
@@ -325,28 +300,6 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const toggleWorkflow = (isGlobal: boolean, workflowPath: string, enabled: boolean) => {
|
||||
FileServiceClient.toggleWorkflow(
|
||||
ToggleWorkflowRequest.create({
|
||||
workflowPath,
|
||||
enabled,
|
||||
scope: isGlobal ? RuleScope.GLOBAL : RuleScope.LOCAL,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.toggles) {
|
||||
if (isGlobal) {
|
||||
setGlobalWorkflowToggles(response.toggles)
|
||||
} else {
|
||||
setLocalWorkflowToggles(response.toggles)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.error("Failed to toggle workflow:", err)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle toggle for remote rules
|
||||
const toggleRemoteRule = (ruleName: string, enabled: boolean) => {
|
||||
FileServiceClient.toggleClineRule(
|
||||
@@ -367,25 +320,6 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// Handle toggle for remote workflows
|
||||
const toggleRemoteWorkflow = (workflowName: string, enabled: boolean) => {
|
||||
FileServiceClient.toggleWorkflow(
|
||||
ToggleWorkflowRequest.create({
|
||||
workflowPath: workflowName,
|
||||
enabled,
|
||||
scope: RuleScope.REMOTE,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.toggles) {
|
||||
setRemoteWorkflowToggles(response.toggles)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error toggling remote workflow:", error)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle toggle for skills
|
||||
const toggleSkill = (isGlobal: boolean, skillPath: string, enabled: boolean) => {
|
||||
FileServiceClient.toggleSkill(
|
||||
@@ -435,11 +369,11 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
<div className="inline-flex min-w-0 max-w-full items-center" ref={modalRef}>
|
||||
<div className="inline-flex w-full items-center" ref={buttonRef}>
|
||||
<Tooltip>
|
||||
{!isVisible && <TooltipContent>Manage Cline Rules & Workflows</TooltipContent>}
|
||||
{!isVisible && <TooltipContent>Manage Cline Rules, Hooks & Skills</TooltipContent>}
|
||||
<TooltipTrigger>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label={isVisible ? "Hide Cline Rules & Workflows" : "Show Cline Rules & Workflows"}
|
||||
aria-label={isVisible ? "Hide Cline Rules, Hooks & Skills" : "Show Cline Rules, Hooks & Skills"}
|
||||
className="p-0 m-0 flex items-center"
|
||||
onClick={() => setIsVisible(!isVisible)}>
|
||||
<i className="codicon codicon-law" style={{ fontSize: "12.5px" }} />
|
||||
@@ -470,9 +404,6 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
<TabButton isActive={currentView === "rules"} onClick={() => setCurrentView("rules")}>
|
||||
Rules
|
||||
</TabButton>
|
||||
<TabButton isActive={currentView === "workflows"} onClick={() => setCurrentView("workflows")}>
|
||||
Workflows
|
||||
</TabButton>
|
||||
{hooksEnabled && (
|
||||
<TabButton isActive={currentView === "hooks"} onClick={() => setCurrentView("hooks")}>
|
||||
Hooks
|
||||
@@ -485,14 +416,10 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Remote config banner */}
|
||||
{(currentView === "rules" && hasRemoteRules) || (currentView === "workflows" && hasRemoteWorkflows) ? (
|
||||
{currentView === "rules" && hasRemoteRules ? (
|
||||
<div className="flex items-center gap-2 px-3 py-3 mb-4 bg-vscode-textBlockQuote-background border-l-[3px] border-vscode-textLink-foreground">
|
||||
<i className="codicon codicon-lock text-sm" />
|
||||
<span className="text-base">
|
||||
{currentView === "rules"
|
||||
? "Your organization manages some rules"
|
||||
: "Your organization manages some workflows"}
|
||||
</span>
|
||||
<span className="text-base">Your organization manages some rules</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -509,17 +436,6 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
Docs
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
) : currentView === "workflows" ? (
|
||||
<p>
|
||||
Workflows allow you to define a series of steps to guide Cline through a repetitive set of
|
||||
tasks, such as deploying a service or submitting a PR. To invoke a workflow, type{" "}
|
||||
<span className="text-foreground font-bold">/workflow-name</span> in the chat.{" "}
|
||||
<VSCodeLink
|
||||
className="text-xs inline"
|
||||
href="https://docs.cline.bot/features/slash-commands/workflows">
|
||||
Docs
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
) : currentView === "skills" ? (
|
||||
<p>
|
||||
Skills are reusable instruction sets that Cline can activate on-demand. When a task matches a
|
||||
@@ -621,63 +537,6 @@ const ClineRulesToggleModal: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : currentView === "workflows" ? (
|
||||
<>
|
||||
{/* Remote Workflows Section */}
|
||||
{hasRemoteWorkflows && (
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Enterprise Workflows</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
{remoteGlobalWorkflows.map((workflow) => {
|
||||
const enabled =
|
||||
workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] === true
|
||||
return (
|
||||
<RuleRow
|
||||
alwaysEnabled={workflow.alwaysEnabled}
|
||||
enabled={enabled}
|
||||
isGlobal={false}
|
||||
isRemote={true}
|
||||
key={workflow.name}
|
||||
rulePath={workflow.name}
|
||||
ruleType="workflow"
|
||||
toggleRule={toggleRemoteWorkflow}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Workflows Section */}
|
||||
<div className="mb-3">
|
||||
<div className="text-sm font-normal mb-2">Global Workflows</div>
|
||||
|
||||
{/* File-based Global Workflows */}
|
||||
<RulesToggleList
|
||||
isGlobal={true}
|
||||
listGap="small"
|
||||
rules={globalWorkflows}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleWorkflow(true, rulePath, enabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Local Workflows Section */}
|
||||
<div className="-mb-2.5">
|
||||
<div className="text-sm font-normal mb-2">Workspace Workflows</div>
|
||||
<RulesToggleList
|
||||
isGlobal={false}
|
||||
listGap="small"
|
||||
rules={localWorkflows}
|
||||
ruleType={"workflow"}
|
||||
showNewRule={true}
|
||||
showNoRules={false}
|
||||
toggleRule={(rulePath, enabled) => toggleWorkflow(false, rulePath, enabled)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : currentView === "hooks" ? (
|
||||
<>
|
||||
<div className="text-xs text-description mb-4">
|
||||
|
||||
@@ -75,12 +75,9 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalWindsurfRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalAgentsRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalWorkflowToggles: (toggles: Record<string, boolean>) => void
|
||||
setGlobalWorkflowToggles: (toggles: Record<string, boolean>) => void
|
||||
setGlobalSkillsToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalSkillsToggles: (toggles: Record<string, boolean>) => void
|
||||
setRemoteRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setRemoteWorkflowToggles: (toggles: Record<string, boolean>) => void
|
||||
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
|
||||
setTotalTasksSize: (value: number | null) => void
|
||||
setExpandTaskHeader: (value: boolean) => void
|
||||
@@ -248,8 +245,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localCursorRulesToggles: {},
|
||||
localWindsurfRulesToggles: {},
|
||||
localAgentsRulesToggles: {},
|
||||
localWorkflowToggles: {},
|
||||
globalWorkflowToggles: {},
|
||||
shellIntegrationTimeout: 4000,
|
||||
terminalReuseEnabled: true,
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal",
|
||||
@@ -816,10 +811,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localCursorRulesToggles: state.localCursorRulesToggles || {},
|
||||
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
|
||||
localAgentsRulesToggles: state.localAgentsRulesToggles || {},
|
||||
localWorkflowToggles: state.localWorkflowToggles || {},
|
||||
globalWorkflowToggles: state.globalWorkflowToggles || {},
|
||||
remoteRulesToggles: state.remoteRulesToggles || {},
|
||||
remoteWorkflowToggles: state.remoteWorkflowToggles || {},
|
||||
enableCheckpointsSetting: state.enableCheckpointsSetting,
|
||||
currentFocusChainChecklist: state.currentFocusChainChecklist,
|
||||
|
||||
@@ -879,16 +871,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
localAgentsRulesToggles: toggles,
|
||||
})),
|
||||
setLocalWorkflowToggles: (toggles) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
localWorkflowToggles: toggles,
|
||||
})),
|
||||
setGlobalWorkflowToggles: (toggles) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
globalWorkflowToggles: toggles,
|
||||
})),
|
||||
setGlobalSkillsToggles: (toggles) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
@@ -904,11 +886,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
remoteRulesToggles: toggles,
|
||||
})),
|
||||
setRemoteWorkflowToggles: (toggles) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
remoteWorkflowToggles: toggles,
|
||||
})),
|
||||
setMcpTab,
|
||||
setTotalTasksSize,
|
||||
refreshClineModels,
|
||||
|
||||
@@ -151,27 +151,32 @@ describe("slash-commands", () => {
|
||||
]
|
||||
|
||||
it("should include MCP commands in results when no query", () => {
|
||||
const result = getMatchingSlashCommands("", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = getMatchingSlashCommands("", mcpServers)
|
||||
const mcpCommands = result.filter((cmd) => cmd.section === "mcp")
|
||||
expect(mcpCommands).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should filter MCP commands by query prefix", () => {
|
||||
const result = getMatchingSlashCommands("mcp:test", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = getMatchingSlashCommands("mcp:test", mcpServers)
|
||||
const mcpCommands = result.filter((cmd) => cmd.section === "mcp")
|
||||
expect(mcpCommands).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should filter to specific MCP prompt", () => {
|
||||
const result = getMatchingSlashCommands("mcp:test-server:sum", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = getMatchingSlashCommands("mcp:test-server:sum", mcpServers)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].name).toBe("mcp:test-server:summarize")
|
||||
})
|
||||
|
||||
it("should return empty for non-matching MCP query", () => {
|
||||
const result = getMatchingSlashCommands("mcp:nonexistent", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = getMatchingSlashCommands("mcp:nonexistent", mcpServers)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not treat workflow-like filenames as available commands", () => {
|
||||
const result = getMatchingSlashCommands("release", mcpServers)
|
||||
expect(result.some((cmd) => cmd.name === "release.md")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateSlashCommand with MCP servers", () => {
|
||||
@@ -183,22 +188,27 @@ describe("slash-commands", () => {
|
||||
]
|
||||
|
||||
it("should return full for exact MCP command match", () => {
|
||||
const result = validateSlashCommand("mcp:server:prompt", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = validateSlashCommand("mcp:server:prompt", mcpServers)
|
||||
expect(result).toBe("full")
|
||||
})
|
||||
|
||||
it("should return partial for partial MCP command match", () => {
|
||||
const result = validateSlashCommand("mcp:server:pro", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = validateSlashCommand("mcp:server:pro", mcpServers)
|
||||
expect(result).toBe("partial")
|
||||
})
|
||||
|
||||
it("should return partial for server prefix only", () => {
|
||||
const result = validateSlashCommand("mcp:serv", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = validateSlashCommand("mcp:serv", mcpServers)
|
||||
expect(result).toBe("partial")
|
||||
})
|
||||
|
||||
it("should return null for non-matching MCP command", () => {
|
||||
const result = validateSlashCommand("mcp:unknown:cmd", {}, {}, undefined, undefined, mcpServers)
|
||||
const result = validateSlashCommand("mcp:unknown:cmd", mcpServers)
|
||||
expect(result).toBe(null)
|
||||
})
|
||||
|
||||
it("should return null for workflow-like filenames", () => {
|
||||
const result = validateSlashCommand("release.md", mcpServers)
|
||||
expect(result).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,69 +7,6 @@ export type { SlashCommand }
|
||||
export const DEFAULT_SLASH_COMMANDS: SlashCommand[] =
|
||||
PLATFORM_CONFIG.type === PlatformType.VSCODE ? [...BASE_SLASH_COMMANDS, ...VSCODE_ONLY_COMMANDS] : BASE_SLASH_COMMANDS
|
||||
|
||||
export function getWorkflowCommands(
|
||||
localWorkflowToggles: Record<string, boolean>,
|
||||
globalWorkflowToggles: Record<string, boolean>,
|
||||
remoteWorkflowToggles?: Record<string, boolean>,
|
||||
remoteWorkflows?: any[],
|
||||
): SlashCommand[] {
|
||||
const { workflows: localWorkflows, nameSet: localWorkflowNames } = Object.entries(localWorkflowToggles)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.reduce(
|
||||
(acc, [filePath, _]) => {
|
||||
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(/^.*[/\\]/, "")
|
||||
|
||||
// skip if a local workflow with the same name exists
|
||||
if (localWorkflowNames.has(fileName)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: fileName,
|
||||
section: "custom",
|
||||
},
|
||||
] as SlashCommand[]
|
||||
})
|
||||
|
||||
// Add remote workflows that are enabled
|
||||
const remoteWorkflowCommands: SlashCommand[] = []
|
||||
if (remoteWorkflows && remoteWorkflowToggles) {
|
||||
for (const workflow of remoteWorkflows) {
|
||||
// Include if alwaysEnabled or if toggle is not explicitly false
|
||||
const enabled = workflow.alwaysEnabled || remoteWorkflowToggles[workflow.name] !== false
|
||||
if (enabled) {
|
||||
remoteWorkflowCommands.push({
|
||||
name: workflow.name,
|
||||
section: "custom",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workflows = [...localWorkflows, ...globalWorkflows, ...remoteWorkflowCommands]
|
||||
return workflows
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets MCP prompt commands from connected MCP servers
|
||||
* Format: mcp:<server-name>:<prompt-name>
|
||||
@@ -174,22 +111,9 @@ export function shouldShowSlashCommandsMenu(text: string, cursorPosition: number
|
||||
/**
|
||||
* Gets filtered slash commands that match the current input
|
||||
*/
|
||||
export function getMatchingSlashCommands(
|
||||
query: string,
|
||||
localWorkflowToggles: Record<string, boolean> = {},
|
||||
globalWorkflowToggles: Record<string, boolean> = {},
|
||||
remoteWorkflowToggles?: Record<string, boolean>,
|
||||
remoteWorkflows?: any[],
|
||||
mcpServers: McpServer[] = [],
|
||||
): SlashCommand[] {
|
||||
const workflowCommands = getWorkflowCommands(
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteWorkflows,
|
||||
)
|
||||
export function getMatchingSlashCommands(query: string, mcpServers: McpServer[] = []): SlashCommand[] {
|
||||
const mcpPromptCommands = getMcpPromptCommands(mcpServers)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands]
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...mcpPromptCommands]
|
||||
|
||||
if (!query) {
|
||||
return allCommands
|
||||
@@ -226,26 +150,13 @@ export function insertSlashCommand(
|
||||
* Determines the validation state of a slash command
|
||||
* Returns partial if we have a partial match against valid commands, or full for full match
|
||||
*/
|
||||
export function validateSlashCommand(
|
||||
command: string,
|
||||
localWorkflowToggles: Record<string, boolean> = {},
|
||||
globalWorkflowToggles: Record<string, boolean> = {},
|
||||
remoteWorkflowToggles?: Record<string, boolean>,
|
||||
remoteWorkflows?: any[],
|
||||
mcpServers: McpServer[] = [],
|
||||
): "full" | "partial" | null {
|
||||
export function validateSlashCommand(command: string, mcpServers: McpServer[] = []): "full" | "partial" | null {
|
||||
if (!command) {
|
||||
return null
|
||||
}
|
||||
|
||||
const workflowCommands = getWorkflowCommands(
|
||||
localWorkflowToggles,
|
||||
globalWorkflowToggles,
|
||||
remoteWorkflowToggles,
|
||||
remoteWorkflows,
|
||||
)
|
||||
const mcpPromptCommands = getMcpPromptCommands(mcpServers)
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...workflowCommands, ...mcpPromptCommands]
|
||||
const allCommands = [...DEFAULT_SLASH_COMMANDS, ...mcpPromptCommands]
|
||||
|
||||
// case insensitive matching
|
||||
const exactMatch = allCommands.some((cmd) => cmd.name.toLowerCase() === command.toLowerCase())
|
||||
|
||||
Reference in New Issue
Block a user