Merge branch 'main' into vs/support-unbound

This commit is contained in:
pugazhendhi-m
2025-01-28 21:58:23 +05:30
committed by GitHub
41 changed files with 1691 additions and 180 deletions

View File

@@ -28,6 +28,8 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
setAlwaysAllowBrowser,
alwaysAllowMcp,
setAlwaysAllowMcp,
alwaysAllowModeSwitch,
setAlwaysAllowModeSwitch,
alwaysApproveResubmit,
setAlwaysApproveResubmit,
autoApprovalEnabled,
@@ -71,6 +73,13 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
enabled: alwaysAllowMcp ?? false,
description: "Allows use of configured MCP servers which may modify filesystem or interact with APIs.",
},
{
id: "switchModes",
label: "Switch between modes",
shortName: "Modes",
enabled: alwaysAllowModeSwitch ?? false,
description: "Allows automatic switching between different AI modes without requiring approval.",
},
{
id: "retryRequests",
label: "Retry failed requests",
@@ -120,6 +129,12 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
vscode.postMessage({ type: "alwaysAllowMcp", bool: newValue })
}, [alwaysAllowMcp, setAlwaysAllowMcp])
const handleModeSwitchChange = useCallback(() => {
const newValue = !(alwaysAllowModeSwitch ?? false)
setAlwaysAllowModeSwitch(newValue)
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: newValue })
}, [alwaysAllowModeSwitch, setAlwaysAllowModeSwitch])
const handleRetryChange = useCallback(() => {
const newValue = !(alwaysApproveResubmit ?? false)
setAlwaysApproveResubmit(newValue)
@@ -133,6 +148,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
executeCommands: handleExecuteChange,
useBrowser: handleBrowserChange,
useMcp: handleMcpChange,
switchModes: handleModeSwitchChange,
retryRequests: handleRetryChange,
}

View File

@@ -55,6 +55,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
mode,
setMode,
autoApprovalEnabled,
alwaysAllowModeSwitch,
} = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
@@ -565,7 +566,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
(alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
(alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
(alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) ||
(alwaysAllowModeSwitch &&
message.ask === "tool" &&
JSON.parse(message.text || "{}")?.tool === "switchMode")
)
},
[
@@ -579,6 +583,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
isAllowedCommand,
alwaysAllowMcp,
isMcpToolAlwaysAllowed,
alwaysAllowModeSwitch,
],
)
@@ -915,6 +920,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
cacheWrites={apiMetrics.totalCacheWrites}
cacheReads={apiMetrics.totalCacheReads}
totalCost={apiMetrics.totalCost}
contextTokens={apiMetrics.contextTokens}
onClose={handleTaskCloseButtonClick}
/>
) : (

View File

@@ -16,6 +16,7 @@ interface TaskHeaderProps {
cacheWrites?: number
cacheReads?: number
totalCost: number
contextTokens: number
onClose: () => void
}
@@ -27,6 +28,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
cacheWrites,
cacheReads,
totalCost,
contextTokens,
onClose,
}) => {
const { apiConfiguration } = useExtensionState()
@@ -272,6 +274,13 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
{!isCostAvailable && <ExportButton />}
</div>
<div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
<span style={{ fontWeight: "bold" }}>Context:</span>
<span style={{ display: "flex", alignItems: "center", gap: "3px" }}>
{contextTokens ? formatLargeNumber(contextTokens) : '-'}
</span>
</div>
{shouldShowPromptCacheInfo && (cacheReads !== undefined || cacheWrites !== undefined) && (
<div style={{ display: "flex", alignItems: "center", gap: "4px", flexWrap: "wrap" }}>
<span style={{ fontWeight: "bold" }}>Cache:</span>

View File

@@ -313,4 +313,168 @@ describe("ChatView - Auto Approval Tests", () => {
})
})
})
it("auto-approves mode switch when enabled", async () => {
render(
<ExtensionStateContextProvider>
<ChatView
isHidden={false}
showAnnouncement={false}
hideAnnouncement={() => {}}
showHistoryView={() => {}}
/>
</ExtensionStateContextProvider>,
)
// First hydrate state with initial task
mockPostMessage({
alwaysAllowModeSwitch: true,
autoApprovalEnabled: true,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
],
})
// Then send the mode switch ask message
mockPostMessage({
alwaysAllowModeSwitch: true,
autoApprovalEnabled: true,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "tool",
ts: Date.now(),
text: JSON.stringify({ tool: "switchMode" }),
partial: false,
},
],
})
// Wait for the auto-approval message
await waitFor(() => {
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "askResponse",
askResponse: "yesButtonClicked",
})
})
})
it("does not auto-approve mode switch when disabled", async () => {
render(
<ExtensionStateContextProvider>
<ChatView
isHidden={false}
showAnnouncement={false}
hideAnnouncement={() => {}}
showHistoryView={() => {}}
/>
</ExtensionStateContextProvider>,
)
// First hydrate state with initial task
mockPostMessage({
alwaysAllowModeSwitch: false,
autoApprovalEnabled: true,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
],
})
// Then send the mode switch ask message
mockPostMessage({
alwaysAllowModeSwitch: false,
autoApprovalEnabled: true,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "tool",
ts: Date.now(),
text: JSON.stringify({ tool: "switchMode" }),
partial: false,
},
],
})
// Verify no auto-approval message was sent
expect(vscode.postMessage).not.toHaveBeenCalledWith({
type: "askResponse",
askResponse: "yesButtonClicked",
})
})
it("does not auto-approve mode switch when auto-approval is disabled", async () => {
render(
<ExtensionStateContextProvider>
<ChatView
isHidden={false}
showAnnouncement={false}
hideAnnouncement={() => {}}
showHistoryView={() => {}}
/>
</ExtensionStateContextProvider>,
)
// First hydrate state with initial task
mockPostMessage({
alwaysAllowModeSwitch: true,
autoApprovalEnabled: false,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
],
})
// Then send the mode switch ask message
mockPostMessage({
alwaysAllowModeSwitch: true,
autoApprovalEnabled: false,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "tool",
ts: Date.now(),
text: JSON.stringify({ tool: "switchMode" }),
partial: false,
},
],
})
// Verify no auto-approval message was sent
expect(vscode.postMessage).not.toHaveBeenCalledWith({
type: "askResponse",
askResponse: "yesButtonClicked",
})
})
})

View File

@@ -120,7 +120,7 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
if (!node.lang) {
node.lang = "javascript"
} else if (node.lang.includes(".")) {
// if the langauge is a file, get the extension
// if the language is a file, get the extension
node.lang = node.lang.split(".").slice(-1)[0]
}
})

View File

@@ -0,0 +1,36 @@
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
interface ExperimentalFeatureProps {
name: string
description: string
enabled: boolean
onChange: (value: boolean) => void
}
const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => {
return (
<div
style={{
marginTop: 10,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ color: "var(--vscode-errorForeground)" }}></span>
<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
<span style={{ fontWeight: "500" }}>{name}</span>
</VSCodeCheckbox>
</div>
<p
style={{
fontSize: "12px",
marginBottom: 15,
color: "var(--vscode-descriptionForeground)",
}}>
{description}
</p>
</div>
)
}
export default ExperimentalFeature

View File

@@ -25,6 +25,7 @@ const OpenAiModelPicker: React.FC = () => {
}
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
setSearchTerm(newModelId)
}
useEffect(() => {

View File

@@ -4,6 +4,8 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import ApiOptions from "./ApiOptions"
import ExperimentalFeature from "./ExperimentalFeature"
import { EXPERIMENT_IDS, experimentConfigsMap } from "../../../../src/shared/experiments"
import ApiConfigManager from "./ApiConfigManager"
type SettingsViewProps = {
@@ -51,8 +53,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setRequestDelaySeconds,
currentApiConfigName,
listApiConfigMeta,
experimentalDiffStrategy,
setExperimentalDiffStrategy,
experiments,
setExperimentEnabled,
alwaysAllowModeSwitch,
setAlwaysAllowModeSwitch,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -92,7 +96,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
text: currentApiConfigName,
apiConfiguration,
})
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
vscode.postMessage({
type: "updateExperimental",
values: experiments,
})
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
onDone()
}
}
@@ -328,6 +338,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
checked={alwaysAllowModeSwitch}
onChange={(e: any) => setAlwaysAllowModeSwitch(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Always approve mode switching</span>
</VSCodeCheckbox>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Automatically switch between different AI modes without requiring approval
</p>
</div>
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
checked={alwaysAllowExecute}
@@ -569,7 +590,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setDiffEnabled(e.target.checked)
if (!e.target.checked) {
// Reset experimental strategy when diffs are disabled
setExperimentalDiffStrategy(false)
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
}
}}>
<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
@@ -585,35 +606,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
{diffEnabled && (
<div
style={{
marginTop: 10,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
<VSCodeCheckbox
checked={experimentalDiffStrategy}
onChange={(e: any) => setExperimentalDiffStrategy(e.target.checked)}>
<span style={{ fontWeight: "500" }}>
Use experimental unified diff strategy
</span>
</VSCodeCheckbox>
</div>
<p
style={{
fontSize: "12px",
marginBottom: 15,
color: "var(--vscode-descriptionForeground)",
}}>
Enable the experimental unified diff strategy. This strategy might reduce the number
of retries caused by model errors but may cause unexpected behavior or incorrect
edits. Only enable if you understand the risks and are willing to carefully review
all changes.
</p>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<div style={{ marginTop: 10 }}>
<ExperimentalFeature
key={EXPERIMENT_IDS.DIFF_STRATEGY}
{...experimentConfigsMap.DIFF_STRATEGY}
enabled={experiments[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false}
onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)}
/>
<div style={{ display: "flex", alignItems: "center", gap: "5px", marginTop: "15px" }}>
<span style={{ fontWeight: "500", minWidth: "100px" }}>Match precision</span>
<input
type="range"
@@ -646,6 +646,23 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
)}
{Object.entries(experimentConfigsMap)
.filter((config) => config[0] !== "DIFF_STRATEGY")
.map((config) => (
<ExperimentalFeature
key={config[0]}
{...config[1]}
enabled={
experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false
}
onChange={(enabled) =>
setExperimentEnabled(
EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS],
enabled,
)
}
/>
))}
</div>
</div>

View File

@@ -16,6 +16,7 @@ import { McpServer } from "../../../src/shared/mcp"
import { checkExistKey } from "../../../src/shared/checkExistApiConfig"
import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes"
import { CustomSupportPrompts } from "../../../src/shared/support-prompt"
import { experimentDefault, ExperimentId } from "../../../src/shared/experiments"
export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -33,6 +34,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setAlwaysAllowExecute: (value: boolean) => void
setAlwaysAllowBrowser: (value: boolean) => void
setAlwaysAllowMcp: (value: boolean) => void
setAlwaysAllowModeSwitch: (value: boolean) => void
setShowAnnouncement: (value: boolean) => void
setAllowedCommands: (value: string[]) => void
setSoundEnabled: (value: boolean) => void
@@ -62,9 +64,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setCustomSupportPrompts: (value: CustomSupportPrompts) => void
enhancementApiConfigId?: string
setEnhancementApiConfigId: (value: string) => void
experimentalDiffStrategy: boolean
setExperimentalDiffStrategy: (value: boolean) => void
autoApprovalEnabled?: boolean
setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
setAutoApprovalEnabled: (value: boolean) => void
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
customModes: ModeConfig[]
@@ -97,8 +97,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
mode: defaultModeSlug,
customModePrompts: defaultPrompts,
customSupportPrompts: {},
experiments: experimentDefault,
enhancementApiConfigId: "",
experimentalDiffStrategy: false,
autoApprovalEnabled: false,
customModes: [],
})
@@ -241,7 +241,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
writeDelayMs: state.writeDelayMs,
screenshotQuality: state.screenshotQuality,
experimentalDiffStrategy: state.experimentalDiffStrategy ?? false,
setExperimentEnabled: (id, enabled) =>
setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
setApiConfiguration: (value) =>
setState((prevState) => ({
...prevState,
@@ -253,6 +254,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })),
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
@@ -277,8 +279,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
setEnhancementApiConfigId: (value) =>
setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
setExperimentalDiffStrategy: (value) =>
setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })),
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
handleInputChange,
setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),