Compare commits

...

3 Commits

Author SHA1 Message Date
0xtoshii ac817836a0 update state 2025-08-01 17:52:58 -07:00
0xtoshii 1eea39ac16 base messaging implementation & ui 2025-08-01 16:51:52 -07:00
0xtoshii 619c33adf0 base implementation 2025-08-01 14:54:54 -07:00
10 changed files with 79 additions and 4 deletions
+1
View File
@@ -110,6 +110,7 @@ message UpdateSettingsRequest {
optional PlanActMode mode = 13;
optional string preferred_language = 14;
optional string openai_reasoning_effort = 15;
optional bool strict_plan_mode_enabled = 16;
}
// Complete API Configuration message
+5 -1
View File
@@ -180,6 +180,7 @@ export class Controller {
enableCheckpointsSetting,
isNewUser,
taskHistory,
strictPlanModeEnabled,
} = await getAllExtensionState(this.context)
const NEW_USER_TASK_COUNT_THRESHOLD = 10
@@ -211,6 +212,7 @@ export class Controller {
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled ?? false,
shellIntegrationTimeout,
terminalReuseEnabled ?? true,
terminalOutputLineLimit ?? 500,
@@ -292,7 +294,7 @@ export class Controller {
await this.postStateToWebview()
if (this.task) {
this.task.mode = modeToSwitchTo
this.task.updateMode(modeToSwitchTo)
if (this.task.taskState.isAwaitingPlanResponse && didSwitchToActMode) {
this.task.taskState.didRespondToPlanAskBySwitchingMode = true
// Use chatContent if provided, otherwise use default message
@@ -731,6 +733,7 @@ export class Controller {
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled,
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
@@ -783,6 +786,7 @@ export class Controller {
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled,
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
+9 -1
View File
@@ -58,7 +58,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
if (request.mode !== undefined) {
const mode = request.mode === PlanActMode.PLAN ? "plan" : "act"
if (controller.task) {
controller.task.mode = mode
controller.task.updateMode(mode)
}
await controller.context.globalState.update("mode", request.mode)
}
@@ -92,6 +92,14 @@ export async function updateSettings(controller: Controller, request: UpdateSett
await controller.context.globalState.update("terminalOutputLineLimit", Number(request.terminalOutputLineLimit))
}
// Update strict plan mode setting
if (request.strictPlanModeEnabled !== undefined) {
if (controller.task) {
controller.task.updateStrictPlanMode(request.strictPlanModeEnabled)
}
await controller.context.globalState.update("strictPlanModeEnabled", request.strictPlanModeEnabled)
}
// Post updated state to webview
await controller.postStateToWebview()
+1
View File
@@ -83,6 +83,7 @@ export type GlobalStateKey =
| "sapAiCoreBaseUrl"
| "sapAiResourceGroup"
| "claudeCodePath"
| "strictPlanModeEnabled"
// Settings around plan/act and ephemeral model configuration
| "preferredLanguage"
| "openaiReasoningEffort"
+3
View File
@@ -283,6 +283,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
@@ -341,6 +342,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "preferredLanguage") as Promise<string | undefined>,
getGlobalState(context, "openaiReasoningEffort") as Promise<OpenaiReasoningEffort | undefined>,
getGlobalState(context, "mode") as Promise<Mode | undefined>,
getGlobalState(context, "strictPlanModeEnabled") as Promise<boolean | undefined>,
// Plan mode configurations
getGlobalState(context, "planModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "planModeApiModelId") as Promise<string | undefined>,
@@ -560,6 +562,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
preferredLanguage: preferredLanguage || "English",
openaiReasoningEffort: (openaiReasoningEffort as OpenaiReasoningEffort) || "medium",
mode: mode || "act",
strictPlanModeEnabled: strictPlanModeEnabled ?? false,
userInfo,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
+26
View File
@@ -94,6 +94,7 @@ export class ToolExecutor {
private cwd: string,
private taskId: string,
private mode: Mode,
private strictPlanModeEnabled: boolean,
// Callbacks to the Task (Entity)
private say: (
@@ -124,6 +125,22 @@ export class ToolExecutor {
this.autoApprover.updateSettings(settings)
}
/**
* Defines the tools which should be restricted in plan mode
*/
private isPlanModeToolRestricted(toolName: ToolUseName): boolean {
const planModeRestrictedTools: ToolUseName[] = ["write_to_file", "replace_in_file"]
return planModeRestrictedTools.includes(toolName)
}
public updateMode(mode: Mode): void {
this.mode = mode
}
public updateStrictPlanModeEnabled(strictPlanModeEnabled: boolean): void {
this.strictPlanModeEnabled = strictPlanModeEnabled
}
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
const isNextGenModel =
isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api) || isGrok4ModelFamily(this.api)
@@ -437,6 +454,15 @@ export class ToolExecutor {
return
}
// Logic for plan-model tool call restrictions
if (this.strictPlanModeEnabled && this.mode === "plan" && block.name && this.isPlanModeToolRestricted(block.name)) {
const errorMessage = `Tool '${block.name}' is not available in PLAN MODE. This tool is restricted to ACT MODE for file modifications. Only use tools available for PLAN MODE when in that mode.`
await this.say("error", errorMessage)
this.pushToolResult(formatResponse.toolError(errorMessage), block)
await this.saveCheckpoint()
return
}
if (block.name !== "browser_action") {
await this.browserSession.closeBrowser()
}
+11
View File
@@ -157,6 +157,7 @@ export class Task {
preferredLanguage: string,
openaiReasoningEffort: OpenaiReasoningEffort,
mode: Mode,
strictPlanModeEnabled: boolean,
shellIntegrationTimeout: number,
terminalReuseEnabled: boolean,
terminalOutputLineLimit: number,
@@ -330,6 +331,7 @@ export class Task {
cwd,
this.taskId,
this.mode,
strictPlanModeEnabled,
this.say.bind(this),
this.ask.bind(this),
this.saveCheckpoint.bind(this),
@@ -340,6 +342,15 @@ export class Task {
)
}
public updateMode(mode: Mode): void {
this.mode = mode
this.toolExecutor.updateMode(mode)
}
public updateStrictPlanMode(strictPlanModeEnabled: boolean): void {
this.toolExecutor.updateStrictPlanModeEnabled(strictPlanModeEnabled)
}
// While a task is ref'd by a controller, it will always have access to the extension context
// This error is thrown if the controller derefs the task after e.g., aborting the task
private getContext(): vscode.ExtensionContext {
+1
View File
@@ -62,6 +62,7 @@ export interface ExtensionState {
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
mcpResponsesCollapsed?: boolean
strictPlanModeEnabled?: boolean
}
export interface ClineMessage {
@@ -12,8 +12,14 @@ interface FeatureSettingsSectionProps {
}
const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionProps) => {
const { enableCheckpointsSetting, mcpMarketplaceEnabled, mcpDisplayMode, mcpResponsesCollapsed, openaiReasoningEffort } =
useExtensionState()
const {
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpDisplayMode,
mcpResponsesCollapsed,
openaiReasoningEffort,
strictPlanModeEnabled,
} = useExtensionState()
const handleReasoningEffortChange = (newValue: OpenaiReasoningEffort) => {
updateSetting("openaiReasoningEffort", newValue)
@@ -103,6 +109,19 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
Reasoning effort for the OpenAI family of models(applies to all OpenAI model providers)
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={strictPlanModeEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
updateSetting("strictPlanModeEnabled", checked)
}}>
Enable strict plan mode
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Enforces strict tool use while in plan mode, preventing file edits.
</p>
</div>
</div>
</Section>
</div>
@@ -196,6 +196,7 @@ export const ExtensionStateContextProvider: React.FC<{
isNewUser: false,
welcomeViewCompleted: false,
mcpResponsesCollapsed: false, // Default value (expanded), will be overwritten by extension state
strictPlanModeEnabled: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)