Skip to content

Commit 72fe12d

Browse files
committed
Refactor modes
1 parent b35206b commit 72fe12d

39 files changed

+909
-4310
lines changed

src/core/Cline.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ import { arePathsEqual, getReadablePath } from "../utils/path"
4545
import { parseMentions } from "./mentions"
4646
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
4747
import { formatResponse } from "./prompts/responses"
48-
import { addCustomInstructions, codeMode, SYSTEM_PROMPT } from "./prompts/system"
48+
import { addCustomInstructions, SYSTEM_PROMPT } from "./prompts/system"
49+
import { modes, defaultModeSlug } from "../shared/modes"
4950
import { truncateHalfConversation } from "./sliding-window"
5051
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
5152
import { detectCodeOmission } from "../integrations/editor/detect-omission"
@@ -1126,7 +1127,7 @@ export class Cline {
11261127
// Validate tool use based on current mode
11271128
const { mode } = await this.providerRef.deref()?.getState() ?? {}
11281129
try {
1129-
validateToolUse(block.name, mode ?? codeMode)
1130+
validateToolUse(block.name, mode ?? defaultModeSlug)
11301131
} catch (error) {
11311132
this.consecutiveMistakeCount++
11321133
pushToolResult(formatResponse.toolError(error.message))
@@ -2585,12 +2586,12 @@ export class Cline {
25852586

25862587
// Add current mode and any mode-specific warnings
25872588
const { mode } = await this.providerRef.deref()?.getState() ?? {}
2588-
const currentMode = mode ?? codeMode
2589+
const currentMode = mode ?? defaultModeSlug
25892590
details += `\n\n# Current Mode\n${currentMode}`
2590-
2591+
25912592
// Add warning if not in code mode
25922593
if (!isToolAllowedForMode('write_to_file', currentMode) || !isToolAllowedForMode('execute_command', currentMode)) {
2593-
details += `\n\nNOTE: You are currently in '${currentMode}' mode which only allows read-only operations. To write files or execute commands, the user will need to switch to 'code' mode. Note that only the user can switch modes.`
2594+
details += `\n\nNOTE: You are currently in '${currentMode}' mode which only allows read-only operations. To write files or execute commands, the user will need to switch to '${defaultModeSlug}' mode. Note that only the user can switch modes.`
25942595
}
25952596

25962597
if (includeFileDetails) {
Lines changed: 16 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import { isToolAllowedForMode, validateToolUse } from '../mode-validator'
2-
import { codeMode, architectMode, askMode } from '../prompts/system'
3-
import { CODE_ALLOWED_TOOLS, READONLY_ALLOWED_TOOLS, ToolName } from '../tool-lists'
1+
import { Mode, isToolAllowedForMode, TestToolName, getModeConfig, modes } from '../../shared/modes';
2+
import { validateToolUse } from '../mode-validator';
43

5-
// For testing purposes, we need to handle the 'unknown_tool' case
6-
type TestToolName = ToolName | 'unknown_tool';
7-
8-
// Helper function to safely cast string to TestToolName for testing
9-
function asTestTool(str: string): TestToolName {
10-
return str as TestToolName;
11-
}
4+
const asTestTool = (tool: string): TestToolName => tool as TestToolName;
5+
const [codeMode, architectMode, askMode] = modes.map(mode => mode.slug);
126

137
describe('mode-validator', () => {
148
describe('isToolAllowedForMode', () => {
159
describe('code mode', () => {
1610
it('allows all code mode tools', () => {
17-
CODE_ALLOWED_TOOLS.forEach(tool => {
11+
const mode = getModeConfig(codeMode);
12+
mode.tools.forEach(([tool]) => {
1813
expect(isToolAllowedForMode(tool, codeMode)).toBe(true)
1914
})
2015
})
@@ -25,64 +20,33 @@ describe('mode-validator', () => {
2520
})
2621

2722
describe('architect mode', () => {
28-
it('allows only read-only and MCP tools', () => {
29-
// Test allowed tools
30-
READONLY_ALLOWED_TOOLS.forEach(tool => {
23+
it('allows configured tools', () => {
24+
const mode = getModeConfig(architectMode);
25+
mode.tools.forEach(([tool]) => {
3126
expect(isToolAllowedForMode(tool, architectMode)).toBe(true)
3227
})
33-
34-
// Test specific disallowed tools that we know are in CODE_ALLOWED_TOOLS but not in READONLY_ALLOWED_TOOLS
35-
const disallowedTools = ['execute_command', 'write_to_file', 'apply_diff'] as const;
36-
disallowedTools.forEach(tool => {
37-
expect(isToolAllowedForMode(tool as ToolName, architectMode)).toBe(false)
38-
})
3928
})
4029
})
4130

4231
describe('ask mode', () => {
43-
it('allows only read-only and MCP tools', () => {
44-
// Test allowed tools
45-
READONLY_ALLOWED_TOOLS.forEach(tool => {
32+
it('allows configured tools', () => {
33+
const mode = getModeConfig(askMode);
34+
mode.tools.forEach(([tool]) => {
4635
expect(isToolAllowedForMode(tool, askMode)).toBe(true)
4736
})
48-
49-
// Test specific disallowed tools that we know are in CODE_ALLOWED_TOOLS but not in READONLY_ALLOWED_TOOLS
50-
const disallowedTools = ['execute_command', 'write_to_file', 'apply_diff'] as const;
51-
disallowedTools.forEach(tool => {
52-
expect(isToolAllowedForMode(tool as ToolName, askMode)).toBe(false)
53-
})
5437
})
5538
})
5639
})
5740

5841
describe('validateToolUse', () => {
5942
it('throws error for disallowed tools in architect mode', () => {
60-
expect(() => validateToolUse('write_to_file' as ToolName, architectMode)).toThrow(
61-
'Tool "write_to_file" is not allowed in architect mode.'
43+
expect(() => validateToolUse('unknown_tool', 'architect')).toThrow(
44+
'Tool "unknown_tool" is not allowed in architect mode.'
6245
)
6346
})
6447

65-
it('throws error for disallowed tools in ask mode', () => {
66-
expect(() => validateToolUse('execute_command' as ToolName, askMode)).toThrow(
67-
'Tool "execute_command" is not allowed in ask mode.'
68-
)
69-
})
70-
71-
it('throws error for unknown tools in code mode', () => {
72-
expect(() => validateToolUse(asTestTool('unknown_tool'), codeMode)).toThrow(
73-
'Tool "unknown_tool" is not allowed in code mode.'
74-
)
75-
})
76-
77-
it('does not throw for allowed tools', () => {
78-
// Code mode
79-
expect(() => validateToolUse('write_to_file' as ToolName, codeMode)).not.toThrow()
80-
81-
// Architect mode
82-
expect(() => validateToolUse('read_file' as ToolName, architectMode)).not.toThrow()
83-
84-
// Ask mode
85-
expect(() => validateToolUse('browser_action' as ToolName, askMode)).not.toThrow()
48+
it('does not throw for allowed tools in architect mode', () => {
49+
expect(() => validateToolUse('read_file', 'architect')).not.toThrow()
8650
})
8751
})
8852
})

src/core/diff/strategies/__tests__/new-unified.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('main', () => {
2828
describe('getToolDescription', () => {
2929
it('should return tool description with correct cwd', () => {
3030
const cwd = '/test/path'
31-
const description = strategy.getToolDescription(cwd)
31+
const description = strategy.getToolDescription({ cwd })
3232

3333
expect(description).toContain('apply_diff')
3434
expect(description).toContain(cwd)

src/core/diff/strategies/__tests__/search-replace.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,12 +1521,12 @@ function two() {
15211521

15221522
it('should include the current working directory', async () => {
15231523
const cwd = '/test/dir'
1524-
const description = await strategy.getToolDescription(cwd)
1524+
const description = await strategy.getToolDescription({ cwd })
15251525
expect(description).toContain(`relative to the current working directory ${cwd}`)
15261526
})
15271527

15281528
it('should include required format elements', async () => {
1529-
const description = await strategy.getToolDescription('/test')
1529+
const description = await strategy.getToolDescription({ cwd: '/test' })
15301530
expect(description).toContain('<<<<<<< SEARCH')
15311531
expect(description).toContain('=======')
15321532
expect(description).toContain('>>>>>>> REPLACE')
@@ -1535,7 +1535,7 @@ function two() {
15351535
})
15361536

15371537
it('should document start_line and end_line parameters', async () => {
1538-
const description = await strategy.getToolDescription('/test')
1538+
const description = await strategy.getToolDescription({ cwd: '/test' })
15391539
expect(description).toContain('start_line: (required) The line number where the search block starts.')
15401540
expect(description).toContain('end_line: (required) The line number where the search block ends.')
15411541
})

src/core/diff/strategies/__tests__/unified.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('UnifiedDiffStrategy', () => {
1010
describe('getToolDescription', () => {
1111
it('should return tool description with correct cwd', () => {
1212
const cwd = '/test/path'
13-
const description = strategy.getToolDescription(cwd)
13+
const description = strategy.getToolDescription({ cwd })
1414

1515
expect(description).toContain('apply_diff')
1616
expect(description).toContain(cwd)

src/core/diff/strategies/new-unified/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class NewUnifiedDiffStrategy implements DiffStrategy {
107107
return { hunks }
108108
}
109109

110-
getToolDescription(cwd: string): string {
110+
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
111111
return `# apply_diff Tool Rules:
112112
113113
Generate a unified diff similar to what "diff -U0" would produce.
@@ -173,7 +173,7 @@ Format Requirements:
173173
Be precise, consistent, and follow these rules carefully to generate correct diffs!
174174
175175
Parameters:
176-
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${cwd})
176+
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${args.cwd})
177177
- diff: (required) The diff content in unified format to apply to the file.
178178
179179
Usage:

src/core/diff/strategies/search-replace.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class SearchReplaceDiffStrategy implements DiffStrategy {
6565
this.bufferLines = bufferLines ?? BUFFER_LINES;
6666
}
6767

68-
getToolDescription(cwd: string): string {
68+
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
6969
return `## apply_diff
7070
Description: Request to replace existing code using a search and replace block.
7171
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
@@ -76,7 +76,7 @@ If you're not confident in the exact content to search for, use the read_file to
7676
When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
7777
7878
Parameters:
79-
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
79+
- path: (required) The path of the file to modify (relative to the current working directory ${args.cwd})
8080
- diff: (required) The search/replace block defining the changes.
8181
- start_line: (required) The line number where the search block starts.
8282
- end_line: (required) The line number where the search block ends.

src/core/diff/strategies/unified.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { applyPatch } from "diff"
22
import { DiffStrategy, DiffResult } from "../types"
33

44
export class UnifiedDiffStrategy implements DiffStrategy {
5-
getToolDescription(cwd: string): string {
5+
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
66
return `## apply_diff
77
Description: Apply a unified diff to a file at the specified path. This tool is useful when you need to make specific modifications to a file based on a set of changes provided in unified diff format (diff -U3).
88
99
Parameters:
10-
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${cwd})
10+
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${args.cwd})
1111
- diff: (required) The diff content in unified format to apply to the file.
1212
1313
Format Requirements:

src/core/diff/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export type DiffResult =
1515
export interface DiffStrategy {
1616
/**
1717
* Get the tool description for this diff strategy
18-
* @param cwd The current working directory
18+
* @param args The tool arguments including cwd and toolOptions
1919
* @returns The complete tool description including format requirements and examples
2020
*/
21-
getToolDescription(cwd: string): string
21+
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string
2222

2323
/**
2424
* Apply a diff to the original content

src/core/mode-validator.ts

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,7 @@
1-
import { Mode } from './prompts/types'
2-
import { codeMode } from './prompts/system'
3-
import { CODE_ALLOWED_TOOLS, READONLY_ALLOWED_TOOLS, ToolName, ReadOnlyToolName } from './tool-lists'
1+
import { Mode, isToolAllowedForMode, TestToolName, getModeConfig } from '../shared/modes';
42

5-
// Extended tool type that includes 'unknown_tool' for testing
6-
export type TestToolName = ToolName | 'unknown_tool';
7-
8-
// Type guard to check if a tool is a valid tool
9-
function isValidTool(tool: TestToolName): tool is ToolName {
10-
return CODE_ALLOWED_TOOLS.includes(tool as ToolName);
11-
}
12-
13-
// Type guard to check if a tool is a read-only tool
14-
function isReadOnlyTool(tool: TestToolName): tool is ReadOnlyToolName {
15-
return READONLY_ALLOWED_TOOLS.includes(tool as ReadOnlyToolName);
16-
}
17-
18-
export function isToolAllowedForMode(toolName: TestToolName, mode: Mode): boolean {
19-
if (mode === codeMode) {
20-
return isValidTool(toolName);
21-
}
22-
// Both architect and ask modes use the same read-only tools
23-
return isReadOnlyTool(toolName);
24-
}
3+
export { isToolAllowedForMode };
4+
export type { TestToolName };
255

266
export function validateToolUse(toolName: TestToolName, mode: Mode): void {
277
if (!isToolAllowedForMode(toolName, mode)) {

0 commit comments

Comments
 (0)