Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ export interface WebviewMessage {
| "openRuleFile"
| "openRulesDirectory"
text?: string
viewStateId?: string
taskId?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
Expand Down
98 changes: 98 additions & 0 deletions src/core/config/__tests__/importExport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,104 @@ describe("importExport", () => {
expect(mockProvider.settingsImportedAt).toBeUndefined()
})

it("should call broadcastResetToAllInstances after successful import when available", async () => {
const filePath = "/mock/path/settings.json"
const mockFileContent = JSON.stringify({
providerProfiles: {
currentApiConfigName: "valid-profile",
apiConfigs: {
"valid-profile": {
apiProvider: "openai" as ProviderName,
apiKey: "test-key",
id: "valid-id",
},
},
},
globalSettings: { mode: "code" },
})

;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
;(fs.access as Mock).mockResolvedValue(undefined)

mockProviderSettingsManager.export.mockResolvedValue({
currentApiConfigName: "default",
apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
})
mockProviderSettingsManager.listConfig.mockResolvedValue([
{ name: "valid-profile", id: "valid-id", apiProvider: "openai" as ProviderName },
])

const broadcastResetSpy = vi.fn().mockResolvedValue(undefined)
const mockProvider = {
settingsImportedAt: 0,
postStateToWebview: vi.fn().mockResolvedValue(undefined),
broadcastResetToAllInstances: broadcastResetSpy,
}

await importSettingsWithFeedback(
{
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
customModesManager: mockCustomModesManager,
provider: mockProvider,
},
filePath,
)

expect(mockProvider.postStateToWebview).toHaveBeenCalled()
expect(broadcastResetSpy).toHaveBeenCalledTimes(1)

mockProviderSettingsManager.listConfig.mockResolvedValue([])
})

it("should skip broadcastResetToAllInstances when provider does not have it", async () => {
const filePath = "/mock/path/settings.json"
const mockFileContent = JSON.stringify({
providerProfiles: {
currentApiConfigName: "valid-profile",
apiConfigs: {
"valid-profile": {
apiProvider: "openai" as ProviderName,
apiKey: "test-key",
id: "valid-id",
},
},
},
globalSettings: { mode: "code" },
})

;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
;(fs.access as Mock).mockResolvedValue(undefined)

mockProviderSettingsManager.export.mockResolvedValue({
currentApiConfigName: "default",
apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
})
mockProviderSettingsManager.listConfig.mockResolvedValue([
{ name: "valid-profile", id: "valid-id", apiProvider: "openai" as ProviderName },
])

const mockProvider = {
settingsImportedAt: 0,
postStateToWebview: vi.fn().mockResolvedValue(undefined),
// No broadcastResetToAllInstances property
}

await importSettingsWithFeedback(
{
providerSettingsManager: mockProviderSettingsManager,
contextProxy: mockContextProxy,
customModesManager: mockCustomModesManager,
provider: mockProvider,
},
filePath,
)

expect(mockProvider.postStateToWebview).toHaveBeenCalled()

mockProviderSettingsManager.listConfig.mockResolvedValue([])
})

it("should handle multiple profiles with mixed valid and invalid providers", async () => {
;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])

Expand Down
13 changes: 13 additions & 0 deletions src/core/config/importExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type ImportWithProviderOptions = ImportOptions & {
provider: {
settingsImportedAt?: number
postStateToWebview: () => Promise<void>
broadcastResetToAllInstances?(): Promise<void>
}
}

Expand Down Expand Up @@ -385,6 +386,18 @@ export const importSettingsWithFeedback = async (
if (result.success) {
provider.settingsImportedAt = Date.now()
await provider.postStateToWebview()

// Broadcast invalidation to all other live ClineProvider instances so parallel
// tabs don't keep stale view-local state after a settings import.
try {
if (provider.broadcastResetToAllInstances) {
await provider.broadcastResetToAllInstances()
}
} catch (error) {
// Log but do not fail the import if broadcast fails — the import itself succeeded.
console.warn(`Failed to broadcast reset after settings import: ${error}`)
}

provider.settingsImportedAt = undefined
const warnings = "warnings" in result ? result.warnings : undefined

Expand Down
Loading
Loading