Skip to content

Commit 92ad632

Browse files
committed
fix(webview): use merged view-local state in deleteProviderProfile for parallel mode
1 parent de1cb2f commit 92ad632

2 files changed

Lines changed: 124 additions & 2 deletions

File tree

src/core/webview/ClineProvider.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,8 +1824,11 @@ export class ClineProvider
18241824
}
18251825

18261826
async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {
1827-
const globalSettings = this.contextProxy.getValues()
1828-
let profileToActivate: string | undefined = globalSettings.currentApiConfigName
1827+
// Use merged state (view-local + global) so we read THIS TAB's current profile choice,
1828+
// NOT the shared global state which may belong to another parallel tab.
1829+
const { currentApiConfigName: globalCurrentProfile } = await this.getState()
1830+
1831+
let profileToActivate: string | undefined = globalCurrentProfile
18291832

18301833
if (profileToDelete.name === profileToActivate) {
18311834
profileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name
@@ -1837,6 +1840,8 @@ export class ClineProvider
18371840

18381841
const entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)
18391842

1843+
const globalSettings = await this.getState()
1844+
18401845
await this.contextProxy.setValues({
18411846
...globalSettings,
18421847
currentApiConfigName: profileToActivate,

src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,4 +1780,121 @@ describe("ClineProvider - Parallel Mode Support", () => {
17801780
expect(mockPostMessage2).toHaveBeenCalled()
17811781
})
17821782
})
1783+
1784+
describe("deleteProviderProfile", () => {
1785+
it("should use merged state (view-local) when determining profileToActivate in parallel mode", async () => {
1786+
const mockPostMessage1 = vi.fn()
1787+
const mockPostMessage2 = vi.fn()
1788+
1789+
const createMockWebviewView = (postMessage: any) => ({
1790+
webview: {
1791+
postMessage,
1792+
html: "",
1793+
options: {},
1794+
onDidReceiveMessage: vi.fn(),
1795+
asWebviewUri: vi.fn(),
1796+
cspSource: "vscode-webview://test-csp-source",
1797+
},
1798+
visible: true,
1799+
onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
1800+
onDidDispose: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
1801+
})
1802+
1803+
const provider1 = new ClineProvider(
1804+
mockContext,
1805+
mockOutputChannel,
1806+
"sidebar",
1807+
new ContextProxy(mockContext),
1808+
)
1809+
const provider2 = new ClineProvider(mockContext, mockOutputChannel, "editor", new ContextProxy(mockContext))
1810+
1811+
await (provider1 as any).resolveWebviewView(createMockWebviewView(mockPostMessage1))
1812+
await (provider2 as any).resolveWebviewView(createMockWebviewView(mockPostMessage2))
1813+
1814+
// Set different profiles for each provider via saveViewState
1815+
await (provider1 as any).saveViewState("currentApiConfigName", "profile-a")
1816+
await (provider2 as any).saveViewState("currentApiConfigName", "profile-b")
1817+
1818+
// Verify view-local state is isolated
1819+
const state1 = await provider1.getState()
1820+
let state2 = await provider2.getState()
1821+
expect(state1.currentApiConfigName).toBe("profile-a")
1822+
expect(state2.currentApiConfigName).toBe("profile-b")
1823+
1824+
// Set up global state listApiConfigMeta with 3 profiles (including profile-to-delete)
1825+
const profileToDelete = { id: "del-id", name: "profile-to-delete", apiProvider: "anthropic" as const }
1826+
await provider1.contextProxy.setValues({
1827+
listApiConfigMeta: [
1828+
{ id: "a-id", name: "profile-a", apiProvider: "anthropic" },
1829+
{ id: "b-id", name: "profile-b", apiProvider: "anthropic" },
1830+
profileToDelete,
1831+
],
1832+
})
1833+
1834+
// provider2's viewLocalState has currentApiConfigName = "profile-b"
1835+
// When provider2 deletes "profile-to-delete", it should NOT activate "profile-a" (provider1's profile)
1836+
// It should keep "profile-b" because that's what THIS TAB is using
1837+
1838+
// Spy on _updateViewLocalStateFromMutation to capture what profile gets activated
1839+
const updateViewLocalSpy = vi.spyOn(provider2 as any, "_updateViewLocalStateFromMutation")
1840+
1841+
await provider2.deleteProviderProfile(profileToDelete)
1842+
1843+
// The key assertion: profileToActivate should be "profile-b" (provider2's view-local),
1844+
// NOT "profile-a" (provider1's view-local from shared global state).
1845+
// _updateViewLocalStateFromMutation IS called with the merged state's currentApiConfigName.
1846+
expect(updateViewLocalSpy).toHaveBeenCalledWith({ currentApiConfigName: "profile-b" })
1847+
1848+
// Verify provider2's state is still "profile-b" after deletion
1849+
state2 = await provider2.getState()
1850+
expect(state2.currentApiConfigName).toBe("profile-b")
1851+
1852+
await provider1.dispose()
1853+
await provider2.dispose()
1854+
})
1855+
1856+
it("should activate the correct fallback profile when deleting the current profile in parallel mode", async () => {
1857+
const mockPostMessage = vi.fn()
1858+
1859+
const createMockWebviewView = (postMessage: any) => ({
1860+
webview: {
1861+
postMessage,
1862+
html: "",
1863+
options: {},
1864+
onDidReceiveMessage: vi.fn(),
1865+
asWebviewUri: vi.fn(),
1866+
cspSource: "vscode-webview://test-csp-source",
1867+
},
1868+
visible: true,
1869+
onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
1870+
onDidDispose: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
1871+
})
1872+
1873+
const provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
1874+
1875+
await (provider as any).resolveWebviewView(createMockWebviewView(mockPostMessage))
1876+
1877+
// Set up view-local state to use "profile-a"
1878+
await (provider as any).saveViewState("currentApiConfigName", "profile-a")
1879+
1880+
// Set up global state with multiple profiles
1881+
await provider.contextProxy.setValues({
1882+
listApiConfigMeta: [
1883+
{ id: "a-id", name: "profile-a", apiProvider: "anthropic" },
1884+
{ id: "b-id", name: "profile-b", apiProvider: "openrouter" },
1885+
{ id: "c-id", name: "profile-c", apiProvider: "anthropic" },
1886+
],
1887+
})
1888+
1889+
// Delete the currently active profile (profile-a)
1890+
const profileToDelete = { id: "a-id", name: "profile-a", apiProvider: "anthropic" as const }
1891+
await provider.deleteProviderProfile(profileToDelete)
1892+
1893+
// After deletion, should activate to the first remaining profile (profile-b)
1894+
const state = await provider.getState()
1895+
expect(state.currentApiConfigName).toBe("profile-b")
1896+
1897+
await provider.dispose()
1898+
})
1899+
})
17831900
})

0 commit comments

Comments
 (0)