Skip to content

Commit 9464a1b

Browse files
authored
Merge pull request #523 from RooVetGit/allow_architect_mode_to_write_markdown
Allow architect and ask modes to write md files
2 parents a83d8da + 5f8a888 commit 9464a1b

File tree

12 files changed

+577
-52
lines changed

12 files changed

+577
-52
lines changed

src/core/Cline.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,17 +1142,25 @@ export class Cline {
11421142
await this.browserSession.closeBrowser()
11431143
}
11441144

1145-
// Validate tool use before execution
1146-
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
1147-
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
1148-
try {
1149-
validateToolUse(block.name as ToolName, mode ?? defaultModeSlug, customModes ?? [], {
1150-
apply_diff: this.diffEnabled,
1151-
})
1152-
} catch (error) {
1153-
this.consecutiveMistakeCount++
1154-
pushToolResult(formatResponse.toolError(error.message))
1155-
break
1145+
// Only validate complete tool uses
1146+
if (!block.partial) {
1147+
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
1148+
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
1149+
try {
1150+
validateToolUse(
1151+
block.name as ToolName,
1152+
mode ?? defaultModeSlug,
1153+
customModes ?? [],
1154+
{
1155+
apply_diff: this.diffEnabled,
1156+
},
1157+
block.params,
1158+
)
1159+
} catch (error) {
1160+
this.consecutiveMistakeCount++
1161+
pushToolResult(formatResponse.toolError(error.message))
1162+
break
1163+
}
11561164
}
11571165

11581166
switch (block.name) {

src/core/config/CustomModesSchema.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,41 @@ import { TOOL_GROUPS, ToolGroup } from "../../shared/tool-groups"
55
// Create a schema for valid tool groups using the keys of TOOL_GROUPS
66
const ToolGroupSchema = z.enum(Object.keys(TOOL_GROUPS) as [ToolGroup, ...ToolGroup[]])
77

8+
// Schema for group options with regex validation
9+
const GroupOptionsSchema = z.object({
10+
fileRegex: z
11+
.string()
12+
.optional()
13+
.refine(
14+
(pattern) => {
15+
if (!pattern) return true // Optional, so empty is valid
16+
try {
17+
new RegExp(pattern)
18+
return true
19+
} catch {
20+
return false
21+
}
22+
},
23+
{ message: "Invalid regular expression pattern" },
24+
),
25+
description: z.string().optional(),
26+
})
27+
28+
// Schema for a group entry - either a tool group string or a tuple of [group, options]
29+
const GroupEntrySchema = z.union([ToolGroupSchema, z.tuple([ToolGroupSchema, GroupOptionsSchema])])
30+
831
// Schema for array of groups
932
const GroupsArraySchema = z
10-
.array(ToolGroupSchema)
33+
.array(GroupEntrySchema)
1134
.min(1, "At least one tool group is required")
1235
.refine(
1336
(groups) => {
1437
const seen = new Set()
1538
return groups.every((group) => {
16-
if (seen.has(group)) return false
17-
seen.add(group)
39+
// For tuples, check the group name (first element)
40+
const groupName = Array.isArray(group) ? group[0] : group
41+
if (seen.has(groupName)) return false
42+
seen.add(groupName)
1843
return true
1944
})
2045
},

src/core/config/__tests__/CustomModesSchema.test.ts

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { validateCustomMode } from "../CustomModesSchema"
2-
import { ModeConfig } from "../../../shared/modes"
31
import { ZodError } from "zod"
2+
import { CustomModeSchema, validateCustomMode } from "../CustomModesSchema"
3+
import { ModeConfig } from "../../../shared/modes"
44

5-
describe("CustomModesSchema", () => {
5+
describe("CustomModeSchema", () => {
66
describe("validateCustomMode", () => {
77
test("accepts valid mode configuration", () => {
88
const validMode = {
9-
slug: "123e4567-e89b-12d3-a456-426614174000",
9+
slug: "test",
1010
name: "Test Mode",
1111
roleDefinition: "Test role definition",
1212
groups: ["read"] as const,
@@ -17,7 +17,7 @@ describe("CustomModesSchema", () => {
1717

1818
test("accepts mode with multiple groups", () => {
1919
const validMode = {
20-
slug: "123e4567-e89b-12d3-a456-426614174000",
20+
slug: "test",
2121
name: "Test Mode",
2222
roleDefinition: "Test role definition",
2323
groups: ["read", "edit", "browser"] as const,
@@ -28,7 +28,7 @@ describe("CustomModesSchema", () => {
2828

2929
test("accepts mode with optional customInstructions", () => {
3030
const validMode = {
31-
slug: "123e4567-e89b-12d3-a456-426614174000",
31+
slug: "test",
3232
name: "Test Mode",
3333
roleDefinition: "Test role definition",
3434
customInstructions: "Custom instructions",
@@ -119,4 +119,76 @@ describe("CustomModesSchema", () => {
119119
})
120120
})
121121
})
122+
123+
describe("fileRegex", () => {
124+
it("validates a mode with file restrictions and descriptions", () => {
125+
const modeWithJustRegex = {
126+
slug: "markdown-editor",
127+
name: "Markdown Editor",
128+
roleDefinition: "Markdown editing mode",
129+
groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
130+
}
131+
132+
const modeWithDescription = {
133+
slug: "docs-editor",
134+
name: "Documentation Editor",
135+
roleDefinition: "Documentation editing mode",
136+
groups: [
137+
"read",
138+
["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
139+
"browser",
140+
],
141+
}
142+
143+
expect(() => CustomModeSchema.parse(modeWithJustRegex)).not.toThrow()
144+
expect(() => CustomModeSchema.parse(modeWithDescription)).not.toThrow()
145+
})
146+
147+
it("validates file regex patterns", () => {
148+
const validPatterns = ["\\.md$", ".*\\.txt$", "[a-z]+\\.js$"]
149+
const invalidPatterns = ["[", "(unclosed", "\\"]
150+
151+
validPatterns.forEach((pattern) => {
152+
const mode = {
153+
slug: "test",
154+
name: "Test",
155+
roleDefinition: "Test",
156+
groups: ["read", ["edit", { fileRegex: pattern }]],
157+
}
158+
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
159+
})
160+
161+
invalidPatterns.forEach((pattern) => {
162+
const mode = {
163+
slug: "test",
164+
name: "Test",
165+
roleDefinition: "Test",
166+
groups: ["read", ["edit", { fileRegex: pattern }]],
167+
}
168+
expect(() => CustomModeSchema.parse(mode)).toThrow()
169+
})
170+
})
171+
172+
it("prevents duplicate groups", () => {
173+
const modeWithDuplicates = {
174+
slug: "test",
175+
name: "Test",
176+
roleDefinition: "Test",
177+
groups: ["read", "read", ["edit", { fileRegex: "\\.md$" }], ["edit", { fileRegex: "\\.txt$" }]],
178+
}
179+
180+
expect(() => CustomModeSchema.parse(modeWithDuplicates)).toThrow(/Duplicate groups/)
181+
})
182+
183+
it("requires at least one group", () => {
184+
const modeWithNoGroups = {
185+
slug: "test",
186+
name: "Test",
187+
roleDefinition: "Test",
188+
groups: [],
189+
}
190+
191+
expect(() => CustomModeSchema.parse(modeWithNoGroups)).toThrow(/At least one tool group is required/)
192+
})
193+
})
122194
})

src/core/mode-validator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Mode, isToolAllowedForMode, getModeConfig, ModeConfig } from "../shared/modes"
1+
import { Mode, isToolAllowedForMode, getModeConfig, ModeConfig, FileRestrictionError } from "../shared/modes"
22
import { ToolName } from "../shared/tool-groups"
33

44
export { isToolAllowedForMode }
@@ -9,8 +9,9 @@ export function validateToolUse(
99
mode: Mode,
1010
customModes?: ModeConfig[],
1111
toolRequirements?: Record<string, boolean>,
12+
toolParams?: Record<string, unknown>,
1213
): void {
13-
if (!isToolAllowedForMode(toolName, mode, customModes ?? [], toolRequirements)) {
14+
if (!isToolAllowedForMode(toolName, mode, customModes ?? [], toolRequirements, toolParams)) {
1415
throw new Error(`Tool "${toolName}" is not allowed in ${mode} mode.`)
1516
}
1617
}

0 commit comments

Comments
 (0)