Skip to content

Commit b17ae1f

Browse files
committed
feat: add provider-level todo list toggle with advanced settings section
- Add todoListEnabled field to provider settings schema - Implement migration logic for global to provider-level settings - Create TodoListSettingsControl component for UI toggle - Create AdvancedSettingsSection to group diff and todo list settings - Update system prompt generation to respect todoListEnabled setting - Update task creation logic to conditionally include update_todo_list tool - Add comprehensive tests for tool inclusion/exclusion logic - Add translation keys for new settings UI This allows users to disable todo lists at the API profile level, similar to how diff edits can be disabled, with both toggles organized in a collapsible advanced settings section.
1 parent 50e45a2 commit b17ae1f

File tree

13 files changed

+274
-2
lines changed

13 files changed

+274
-2
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
5656
const baseProviderSettingsSchema = z.object({
5757
includeMaxTokens: z.boolean().optional(),
5858
diffEnabled: z.boolean().optional(),
59+
todoListEnabled: z.boolean().optional(),
5960
fuzzyMatchThreshold: z.number().optional(),
6061
modelTemperature: z.number().nullish(),
6162
rateLimitSeconds: z.number().optional(),

src/core/config/ProviderSettingsManager.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const providerProfilesSchema = z.object({
2525
.object({
2626
rateLimitSecondsMigrated: z.boolean().optional(),
2727
diffSettingsMigrated: z.boolean().optional(),
28+
todoListSettingsMigrated: z.boolean().optional(),
2829
openAiHeadersMigrated: z.boolean().optional(),
2930
})
3031
.optional(),
@@ -47,6 +48,7 @@ export class ProviderSettingsManager {
4748
migrations: {
4849
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
4950
diffSettingsMigrated: true, // Mark as migrated on fresh installs
51+
todoListSettingsMigrated: true, // Mark as migrated on fresh installs
5052
openAiHeadersMigrated: true, // Mark as migrated on fresh installs
5153
},
5254
}
@@ -112,6 +114,7 @@ export class ProviderSettingsManager {
112114
providerProfiles.migrations = {
113115
rateLimitSecondsMigrated: false,
114116
diffSettingsMigrated: false,
117+
todoListSettingsMigrated: false,
115118
openAiHeadersMigrated: false,
116119
} // Initialize with default values
117120
isDirty = true
@@ -129,6 +132,12 @@ export class ProviderSettingsManager {
129132
isDirty = true
130133
}
131134

135+
if (!providerProfiles.migrations.todoListSettingsMigrated) {
136+
await this.migrateTodoListSettings(providerProfiles)
137+
providerProfiles.migrations.todoListSettingsMigrated = true
138+
isDirty = true
139+
}
140+
132141
if (!providerProfiles.migrations.openAiHeadersMigrated) {
133142
await this.migrateOpenAiHeaders(providerProfiles)
134143
providerProfiles.migrations.openAiHeadersMigrated = true
@@ -204,6 +213,31 @@ export class ProviderSettingsManager {
204213
}
205214
}
206215

216+
private async migrateTodoListSettings(providerProfiles: ProviderProfiles) {
217+
try {
218+
let todoListEnabled: boolean | undefined
219+
220+
try {
221+
todoListEnabled = await this.context.globalState.get<boolean>("alwaysAllowUpdateTodoList")
222+
} catch (error) {
223+
console.error("[MigrateTodoListSettings] Error getting global todo list settings:", error)
224+
}
225+
226+
if (todoListEnabled === undefined) {
227+
// Failed to get the existing value, use the default.
228+
todoListEnabled = true
229+
}
230+
231+
for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
232+
if (apiConfig.todoListEnabled === undefined) {
233+
apiConfig.todoListEnabled = todoListEnabled
234+
}
235+
}
236+
} catch (error) {
237+
console.error(`[MigrateTodoListSettings] Failed to migrate todo list settings:`, error)
238+
}
239+
}
240+
207241
private async migrateOpenAiHeaders(providerProfiles: ProviderProfiles) {
208242
try {
209243
for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {

src/core/config/__tests__/ProviderSettingsManager.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe("ProviderSettingsManager", () => {
6666
rateLimitSecondsMigrated: true,
6767
diffSettingsMigrated: true,
6868
openAiHeadersMigrated: true,
69+
todoListSettingsMigrated: true,
6970
},
7071
}),
7172
)

src/core/prompts/system.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ async function generatePrompt(
5959
partialReadsEnabled?: boolean,
6060
settings?: Record<string, any>,
6161
todoList?: TodoItem[],
62+
todoListEnabled?: boolean,
6263
): Promise<string> {
6364
if (!context) {
6465
throw new Error("Extension context is required for generating system prompt")
@@ -98,6 +99,7 @@ ${getToolDescriptionsForMode(
9899
experiments,
99100
partialReadsEnabled,
100101
settings,
102+
todoListEnabled,
101103
)}
102104
103105
${getToolUseGuidelinesSection(codeIndexManager)}
@@ -138,6 +140,7 @@ export const SYSTEM_PROMPT = async (
138140
partialReadsEnabled?: boolean,
139141
settings?: Record<string, any>,
140142
todoList?: TodoItem[],
143+
todoListEnabled?: boolean,
141144
): Promise<string> => {
142145
if (!context) {
143146
throw new Error("Extension context is required for generating system prompt")
@@ -205,5 +208,6 @@ ${customInstructions}`
205208
partialReadsEnabled,
206209
settings,
207210
todoList,
211+
todoListEnabled,
208212
)
209213
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// npx vitest src/core/prompts/tools/__tests__/index.spec.ts
2+
3+
import { getToolDescriptionsForMode } from "../index"
4+
5+
describe("getToolDescriptionsForMode", () => {
6+
const mockArgs = {
7+
mode: "code" as const,
8+
cwd: "/test",
9+
supportsComputerUse: false,
10+
codeIndexManager: undefined,
11+
diffStrategy: undefined,
12+
browserViewportSize: undefined,
13+
mcpHub: undefined,
14+
customModes: undefined,
15+
experiments: undefined,
16+
partialReadsEnabled: undefined,
17+
settings: undefined,
18+
}
19+
20+
it("should include update_todo_list tool when todoListEnabled is true", () => {
21+
const tools = getToolDescriptionsForMode(
22+
mockArgs.mode,
23+
mockArgs.cwd,
24+
mockArgs.supportsComputerUse,
25+
mockArgs.codeIndexManager,
26+
mockArgs.diffStrategy,
27+
mockArgs.browserViewportSize,
28+
mockArgs.mcpHub,
29+
mockArgs.customModes,
30+
mockArgs.experiments,
31+
mockArgs.partialReadsEnabled,
32+
mockArgs.settings,
33+
true, // todoListEnabled
34+
)
35+
36+
expect(tools).toContain("update_todo_list")
37+
})
38+
39+
it("should exclude update_todo_list tool when todoListEnabled is false", () => {
40+
const tools = getToolDescriptionsForMode(
41+
mockArgs.mode,
42+
mockArgs.cwd,
43+
mockArgs.supportsComputerUse,
44+
mockArgs.codeIndexManager,
45+
mockArgs.diffStrategy,
46+
mockArgs.browserViewportSize,
47+
mockArgs.mcpHub,
48+
mockArgs.customModes,
49+
mockArgs.experiments,
50+
mockArgs.partialReadsEnabled,
51+
mockArgs.settings,
52+
false, // todoListEnabled
53+
)
54+
55+
expect(tools).not.toContain("update_todo_list")
56+
})
57+
58+
it("should include update_todo_list tool when todoListEnabled is undefined (default true)", () => {
59+
const tools = getToolDescriptionsForMode(
60+
mockArgs.mode,
61+
mockArgs.cwd,
62+
mockArgs.supportsComputerUse,
63+
mockArgs.codeIndexManager,
64+
mockArgs.diffStrategy,
65+
mockArgs.browserViewportSize,
66+
mockArgs.mcpHub,
67+
mockArgs.customModes,
68+
mockArgs.experiments,
69+
mockArgs.partialReadsEnabled,
70+
mockArgs.settings,
71+
undefined, // todoListEnabled
72+
)
73+
74+
expect(tools).toContain("update_todo_list")
75+
})
76+
77+
it("should include other tools regardless of todoListEnabled setting", () => {
78+
const toolsWithTodo = getToolDescriptionsForMode(
79+
mockArgs.mode,
80+
mockArgs.cwd,
81+
mockArgs.supportsComputerUse,
82+
mockArgs.codeIndexManager,
83+
mockArgs.diffStrategy,
84+
mockArgs.browserViewportSize,
85+
mockArgs.mcpHub,
86+
mockArgs.customModes,
87+
mockArgs.experiments,
88+
mockArgs.partialReadsEnabled,
89+
mockArgs.settings,
90+
true, // todoListEnabled
91+
)
92+
93+
const toolsWithoutTodo = getToolDescriptionsForMode(
94+
mockArgs.mode,
95+
mockArgs.cwd,
96+
mockArgs.supportsComputerUse,
97+
mockArgs.codeIndexManager,
98+
mockArgs.diffStrategy,
99+
mockArgs.browserViewportSize,
100+
mockArgs.mcpHub,
101+
mockArgs.customModes,
102+
mockArgs.experiments,
103+
mockArgs.partialReadsEnabled,
104+
mockArgs.settings,
105+
false, // todoListEnabled
106+
)
107+
108+
// Both should have other tools like read_file, write_to_file, etc.
109+
expect(toolsWithTodo.length).toBeGreaterThan(100) // Should have substantial content
110+
expect(toolsWithoutTodo.length).toBeGreaterThan(100) // Should have substantial content
111+
112+
// Tools with todo should be longer than tools without todo
113+
expect(toolsWithTodo.length).toBeGreaterThan(toolsWithoutTodo.length)
114+
115+
// Both should contain common tools
116+
expect(toolsWithTodo).toContain("read_file")
117+
expect(toolsWithoutTodo).toContain("read_file")
118+
})
119+
})

src/core/prompts/tools/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function getToolDescriptionsForMode(
6161
experiments?: Record<string, boolean>,
6262
partialReadsEnabled?: boolean,
6363
settings?: Record<string, any>,
64+
todoListEnabled?: boolean,
6465
): string {
6566
const config = getModeConfig(mode, customModes)
6667
const args: ToolArgs = {
@@ -109,6 +110,11 @@ export function getToolDescriptionsForMode(
109110
tools.delete("codebase_search")
110111
}
111112

113+
// Conditionally exclude update_todo_list if feature is disabled
114+
if (todoListEnabled === false) {
115+
tools.delete("update_todo_list")
116+
}
117+
112118
// Map tool descriptions for allowed tools
113119
const descriptions = Array.from(tools).map((toolName) => {
114120
const descriptionFn = toolDescriptionMap[toolName]

src/core/task/Task.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export type TaskOptions = {
110110
provider: ClineProvider
111111
apiConfiguration: ProviderSettings
112112
enableDiff?: boolean
113+
enableTodoList?: boolean
113114
enableCheckpoints?: boolean
114115
fuzzyMatchThreshold?: number
115116
consecutiveMistakeLimit?: number
@@ -172,6 +173,7 @@ export class Task extends EventEmitter<ClineEvents> {
172173
diffViewProvider: DiffViewProvider
173174
diffStrategy?: DiffStrategy
174175
diffEnabled: boolean = false
176+
todoListEnabled: boolean = true
175177
fuzzyMatchThreshold: number
176178
didEditFile: boolean = false
177179

@@ -213,6 +215,7 @@ export class Task extends EventEmitter<ClineEvents> {
213215
provider,
214216
apiConfiguration,
215217
enableDiff = false,
218+
enableTodoList = true,
216219
enableCheckpoints = true,
217220
fuzzyMatchThreshold = 1.0,
218221
consecutiveMistakeLimit = 3,
@@ -253,6 +256,7 @@ export class Task extends EventEmitter<ClineEvents> {
253256
this.urlContentFetcher = new UrlContentFetcher(provider.context)
254257
this.browserSession = new BrowserSession(provider.context)
255258
this.diffEnabled = enableDiff
259+
this.todoListEnabled = enableTodoList
256260
this.fuzzyMatchThreshold = fuzzyMatchThreshold
257261
this.consecutiveMistakeLimit = consecutiveMistakeLimit
258262
this.providerRef = new WeakRef(provider)
@@ -1649,6 +1653,8 @@ export class Task extends EventEmitter<ClineEvents> {
16491653
{
16501654
maxConcurrentFileReads,
16511655
},
1656+
this.todoList,
1657+
this.todoListEnabled,
16521658
)
16531659
})()
16541660
}

src/core/webview/ClineProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,9 @@ export class ClineProvider
543543
experiments,
544544
} = await this.getState()
545545

546+
// Extract todoListEnabled from provider settings
547+
const todoListEnabled = apiConfiguration.todoListEnabled ?? true
548+
546549
if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
547550
throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
548551
}
@@ -551,6 +554,7 @@ export class ClineProvider
551554
provider: this,
552555
apiConfiguration,
553556
enableDiff,
557+
enableTodoList: todoListEnabled,
554558
enableCheckpoints,
555559
fuzzyMatchThreshold,
556560
task,

src/core/webview/generateSystemPrompt.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
2626
maxConcurrentFileReads,
2727
} = await provider.getState()
2828

29+
// Get todoListEnabled from provider settings with fallback to global setting
30+
const todoListEnabled = apiConfiguration.todoListEnabled ?? true
31+
2932
// Check experiment to determine which diff strategy to use
3033
const isMultiFileApplyDiffEnabled = experimentsModule.isEnabled(
3134
experiments ?? {},
@@ -83,6 +86,8 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
8386
{
8487
maxConcurrentFileReads,
8588
},
89+
undefined, // todoList parameter
90+
todoListEnabled,
8691
)
8792

8893
return systemPrompt
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useState } from "react"
2+
import { useAppTranslation } from "@/i18n/TranslationContext"
3+
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
4+
import { DiffSettingsControl } from "./DiffSettingsControl"
5+
import { TodoListSettingsControl } from "./TodoListSettingsControl"
6+
7+
interface AdvancedSettingsSectionProps {
8+
diffEnabled?: boolean
9+
fuzzyMatchThreshold?: number
10+
todoListEnabled?: boolean
11+
onChange: (field: "diffEnabled" | "fuzzyMatchThreshold" | "todoListEnabled", value: any) => void
12+
}
13+
14+
export const AdvancedSettingsSection: React.FC<AdvancedSettingsSectionProps> = ({
15+
diffEnabled,
16+
fuzzyMatchThreshold,
17+
todoListEnabled,
18+
onChange,
19+
}) => {
20+
const { t } = useAppTranslation()
21+
const [isExpanded, setIsExpanded] = useState(false)
22+
23+
const toggleExpanded = () => {
24+
setIsExpanded(!isExpanded)
25+
}
26+
27+
return (
28+
<div className="flex flex-col gap-3">
29+
<VSCodeButton
30+
appearance="secondary"
31+
onClick={toggleExpanded}
32+
className="flex items-center justify-between w-full text-left">
33+
<span className="font-medium">{t("settings:advanced.section.label")}</span>
34+
<span className="ml-2">{isExpanded ? "▼" : "▶"}</span>
35+
</VSCodeButton>
36+
37+
{isExpanded && (
38+
<div className="flex flex-col gap-4 pl-4 border-l-2 border-vscode-button-background">
39+
<DiffSettingsControl
40+
diffEnabled={diffEnabled}
41+
fuzzyMatchThreshold={fuzzyMatchThreshold}
42+
onChange={onChange}
43+
/>
44+
<TodoListSettingsControl todoListEnabled={todoListEnabled} onChange={onChange} />
45+
</div>
46+
)}
47+
</div>
48+
)
49+
}

0 commit comments

Comments
 (0)