Skip to content

Commit 9f81b99

Browse files
committed
fix: keep current provider profile when restoring a task from history
1 parent f2bdcb6 commit 9f81b99

2 files changed

Lines changed: 85 additions & 36 deletions

File tree

src/core/webview/ClineProvider.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,32 +1088,41 @@ export class ClineProvider
10881088
// This overrides any mode-based config restoration above, because the task's
10891089
// specific provider profile takes precedence over mode defaults.
10901090
if (historyItem.apiConfigName && !skipProfileRestoreFromHistory) {
1091-
const listApiConfig = await this.providerSettingsManager.listConfig()
1092-
// Keep global state/UI in sync with latest profiles for parity with mode restoration above.
1093-
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
1094-
const profile = listApiConfig.find(({ name }) => name === historyItem.apiConfigName)
1091+
const currentApiConfigName = this.contextProxy.getValues().currentApiConfigName
10951092

1096-
if (profile?.name) {
1097-
try {
1098-
if (profile.apiProvider) {
1099-
await this.activateProviderProfile(
1100-
{ name: profile.name },
1101-
{ persistModeConfig: false, persistTaskHistory: false },
1093+
if (currentApiConfigName && currentApiConfigName !== historyItem.apiConfigName) {
1094+
this.log(
1095+
`Keeping current provider profile '${currentApiConfigName}' instead of restoring stale profile '${historyItem.apiConfigName}' for task ${historyItem.id}.`,
1096+
)
1097+
historyItem.apiConfigName = currentApiConfigName
1098+
} else {
1099+
const listApiConfig = await this.providerSettingsManager.listConfig()
1100+
// Keep global state/UI in sync with latest profiles for parity with mode restoration above.
1101+
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
1102+
const profile = listApiConfig.find(({ name }) => name === historyItem.apiConfigName)
1103+
1104+
if (profile?.name) {
1105+
try {
1106+
if (profile.apiProvider) {
1107+
await this.activateProviderProfile(
1108+
{ name: profile.name },
1109+
{ persistModeConfig: false, persistTaskHistory: false },
1110+
)
1111+
}
1112+
} catch (error) {
1113+
// Log the error but continue with task restoration.
1114+
this.log(
1115+
`Failed to restore API configuration '${historyItem.apiConfigName}' for task: ${
1116+
error instanceof Error ? error.message : String(error)
1117+
}. Continuing with current configuration.`,
11021118
)
11031119
}
1104-
} catch (error) {
1105-
// Log the error but continue with task restoration.
1120+
} else {
1121+
// Profile no longer exists, log warning but continue
11061122
this.log(
1107-
`Failed to restore API configuration '${historyItem.apiConfigName}' for task: ${
1108-
error instanceof Error ? error.message : String(error)
1109-
}. Continuing with current configuration.`,
1123+
`Provider profile '${historyItem.apiConfigName}' from history no longer exists. Using current configuration.`,
11101124
)
11111125
}
1112-
} else {
1113-
// Profile no longer exists, log warning but continue
1114-
this.log(
1115-
`Provider profile '${historyItem.apiConfigName}' from history no longer exists. Using current configuration.`,
1116-
)
11171126
}
11181127
} else if (historyItem.apiConfigName && skipProfileRestoreFromHistory) {
11191128
this.log(

src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ describe("ClineProvider - Sticky Provider Profile", () => {
301301

302302
provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
303303

304+
// Seed the ContextProxy state cache with the current profile (initialize() is not
305+
// called in these tests, so the cache starts empty).
306+
await provider.contextProxy.setValue("currentApiConfigName", "default-profile")
307+
304308
// Wait for the async TaskHistoryStore initialization to complete
305309
await new Promise((resolve) => setTimeout(resolve, 10))
306310

@@ -488,10 +492,10 @@ describe("ClineProvider - Sticky Provider Profile", () => {
488492
})
489493

490494
describe("createTaskWithHistoryItem", () => {
491-
it("should restore provider profile from history item when reopening task outside CLI runtime", async () => {
495+
it("should restore provider profile from history item when it matches the current profile", async () => {
492496
await provider.resolveWebviewView(mockWebviewView)
493497

494-
// Create a history item with saved provider profile
498+
// Create a history item with saved provider profile matching the current profile
495499
const historyItem: HistoryItem = {
496500
id: "test-task-id",
497501
number: 1,
@@ -503,7 +507,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
503507
cacheReads: 0,
504508
totalCost: 0.001,
505509
mode: "code",
506-
apiConfigName: "saved-profile", // Saved provider profile
510+
apiConfigName: "default-profile", // Matches currentApiConfigName in beforeEach
507511
}
508512

509513
// Mock activateProviderProfile to track calls
@@ -513,19 +517,55 @@ describe("ClineProvider - Sticky Provider Profile", () => {
513517

514518
// Mock providerSettingsManager.listConfig
515519
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
516-
{ name: "saved-profile", id: "saved-profile-id", apiProvider: "anthropic" },
520+
{ name: "default-profile", id: "default-profile-id", apiProvider: "anthropic" },
517521
])
518522

519523
// Initialize task with history item
520524
await provider.createTaskWithHistoryItem(historyItem)
521525

522526
// Verify provider profile was restored via activateProviderProfile (restore-only: don't persist mode config)
523527
expect(activateProviderProfileSpy).toHaveBeenCalledWith(
524-
{ name: "saved-profile" },
528+
{ name: "default-profile" },
525529
{ persistModeConfig: false, persistTaskHistory: false },
526530
)
527531
})
528532

533+
it("should keep the current provider profile when it differs from the history item's stale profile", async () => {
534+
await provider.resolveWebviewView(mockWebviewView)
535+
536+
// The task was last saved with "saved-profile", but the user has since
537+
// switched to "default-profile" (the current profile in beforeEach).
538+
const historyItem: HistoryItem = {
539+
id: "test-task-id",
540+
number: 1,
541+
ts: Date.now(),
542+
task: "Test task",
543+
tokensIn: 100,
544+
tokensOut: 200,
545+
cacheWrites: 0,
546+
cacheReads: 0,
547+
totalCost: 0.001,
548+
mode: "code",
549+
apiConfigName: "saved-profile",
550+
}
551+
552+
const activateProviderProfileSpy = vi
553+
.spyOn(provider, "activateProviderProfile")
554+
.mockResolvedValue(undefined)
555+
556+
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
557+
{ name: "saved-profile", id: "saved-profile-id", apiProvider: "anthropic" },
558+
])
559+
560+
await provider.createTaskWithHistoryItem(historyItem)
561+
562+
// The stale profile must NOT be reactivated over the user's current selection.
563+
expect(activateProviderProfileSpy).not.toHaveBeenCalledWith({ name: "saved-profile" }, expect.anything())
564+
565+
// The history item's sticky profile is corrected so the new selection persists.
566+
expect(historyItem.apiConfigName).toBe("default-profile")
567+
})
568+
529569
it("should not restore an empty task apiConfigName profile from history", async () => {
530570
await provider.resolveWebviewView(mockWebviewView)
531571

@@ -540,15 +580,15 @@ describe("ClineProvider - Sticky Provider Profile", () => {
540580
cacheReads: 0,
541581
totalCost: 0.001,
542582
mode: "ask",
543-
apiConfigName: "default",
583+
apiConfigName: "default-profile", // Matches current profile so the restore path runs
544584
}
545585

546586
const activateProviderProfileSpy = vi
547587
.spyOn(provider, "activateProviderProfile")
548588
.mockResolvedValue(undefined)
549589

550590
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
551-
{ name: "default", id: "default-id" },
591+
{ name: "default-profile", id: "default-profile-id" },
552592
])
553593

554594
await provider.createTaskWithHistoryItem(historyItem)
@@ -657,7 +697,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
657697
it("should override mode-based config with task's apiConfigName", async () => {
658698
await provider.resolveWebviewView(mockWebviewView)
659699

660-
// Create a history item with both mode and apiConfigName
700+
// Create a history item with both mode and apiConfigName (matching the current profile)
661701
const historyItem: HistoryItem = {
662702
id: "test-task-id",
663703
number: 1,
@@ -669,7 +709,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
669709
cacheReads: 0,
670710
totalCost: 0.001,
671711
mode: "architect", // Mode has a different preferred profile
672-
apiConfigName: "task-specific-profile", // Task's actual profile
712+
apiConfigName: "default-profile", // Task's actual profile (matches currentApiConfigName)
673713
}
674714

675715
// Track all activateProviderProfile calls
@@ -684,14 +724,14 @@ describe("ClineProvider - Sticky Provider Profile", () => {
684724
vi.spyOn(provider.providerSettingsManager, "getModeConfigId").mockResolvedValue("mode-config-id")
685725
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
686726
{ name: "mode-preferred-profile", id: "mode-config-id", apiProvider: "anthropic" },
687-
{ name: "task-specific-profile", id: "task-profile-id", apiProvider: "openai" },
727+
{ name: "default-profile", id: "default-profile-id", apiProvider: "openai" },
688728
])
689729

690730
// Initialize task with history item
691731
await provider.createTaskWithHistoryItem(historyItem)
692732

693733
// Verify task's apiConfigName was activated LAST (overriding mode-based config)
694-
expect(activateCalls[activateCalls.length - 1]).toBe("task-specific-profile")
734+
expect(activateCalls[activateCalls.length - 1]).toBe("default-profile")
695735
})
696736

697737
it("should handle missing provider profile gracefully", async () => {
@@ -708,7 +748,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
708748
cacheWrites: 0,
709749
cacheReads: 0,
710750
totalCost: 0.001,
711-
apiConfigName: "deleted-profile", // Profile that doesn't exist
751+
apiConfigName: "default-profile", // Matches current profile, but profile doesn't exist
712752
}
713753

714754
// Mock providerSettingsManager.listConfig to return empty (profile doesn't exist)
@@ -722,7 +762,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
722762

723763
// Verify a warning was logged
724764
expect(logSpy).toHaveBeenCalledWith(
725-
expect.stringContaining("Provider profile 'deleted-profile' from history no longer exists"),
765+
expect.stringContaining("Provider profile 'default-profile' from history no longer exists"),
726766
)
727767
})
728768
})
@@ -991,12 +1031,12 @@ describe("ClineProvider - Sticky Provider Profile", () => {
9911031
cacheWrites: 0,
9921032
cacheReads: 0,
9931033
totalCost: 0.001,
994-
apiConfigName: "failing-profile",
1034+
apiConfigName: "default-profile", // Matches current profile so the restore path runs
9951035
}
9961036

9971037
// Mock providerSettingsManager.listConfig to return the profile
9981038
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
999-
{ name: "failing-profile", id: "failing-profile-id", apiProvider: "anthropic" },
1039+
{ name: "default-profile", id: "default-profile-id", apiProvider: "anthropic" },
10001040
])
10011041

10021042
// Mock activateProviderProfile to throw error
@@ -1010,7 +1050,7 @@ describe("ClineProvider - Sticky Provider Profile", () => {
10101050

10111051
// Verify error was logged
10121052
expect(logSpy).toHaveBeenCalledWith(
1013-
expect.stringContaining("Failed to restore API configuration 'failing-profile' for task"),
1053+
expect.stringContaining("Failed to restore API configuration 'default-profile' for task"),
10141054
)
10151055
})
10161056
})

0 commit comments

Comments
 (0)