diff --git a/apps/vscode/src/sdk/sdk-cloud-session-coordinator.test.ts b/apps/vscode/src/sdk/sdk-cloud-session-coordinator.test.ts index b82ad64359..761c87fff5 100644 --- a/apps/vscode/src/sdk/sdk-cloud-session-coordinator.test.ts +++ b/apps/vscode/src/sdk/sdk-cloud-session-coordinator.test.ts @@ -768,15 +768,18 @@ describe("SdkCloudSessionCoordinator ownership", () => { checkClinePassSubscription: vi.fn(() => plan.promise), }) - // A local ClinePass model carries over without waiting for the plan check. - expect(coordinator.nextCloudModelId()).toBe("cline-pass/local") + // A local ClinePass model only carries over once the plan check confirms a current plan. + expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID) await vi.waitFor(() => expect(options.postStateToWebview).toHaveBeenCalledTimes(1)) - apiConfig = { actModeApiProvider: "anthropic" } expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID) plan.resolve(true) await vi.waitFor(() => expect(coordinator.clinePassSubscribed()).toBe(true)) expect(options.postStateToWebview).toHaveBeenCalledTimes(2) + expect(coordinator.nextCloudModelId()).toBe("cline-pass/local") + apiConfig = { actModeApiProvider: "cline-pass", actModeClinePassModelId: "cline-free/local" } + expect(coordinator.nextCloudModelId()).toBe("cline-pass/first") + apiConfig = { actModeApiProvider: "anthropic" } expect(coordinator.nextCloudModelId()).toBe("cline-pass/first") apiConfig = { actModeApiProvider: "cline", actModeClineModelId: "usage/model" } expect(coordinator.nextCloudModelId()).toBe("usage/model") @@ -789,6 +792,26 @@ describe("SdkCloudSessionCoordinator ownership", () => { await coordinator.dispose() }) + it("starts a local ClinePass provider user without a current plan on Cline's default model", async () => { + let apiConfig: Record = { actModeApiProvider: "cline-pass", actModeClinePassModelId: "cline-free/local" } + const { coordinator } = makeCoordinator({ + stateManager: { ...makeStateManager(), getApiConfiguration: () => apiConfig } as never, + getAccountScope: () => "user-1:", + loadCloudModels: async () => [ + { id: "cline-pass/local", name: "Local", catalogId: "cline-pass" }, + { id: "cline-free/local", name: "Free", catalogId: "cline" }, + ], + checkClinePassSubscription: async () => false, + }) + + coordinator.nextCloudModelId() + await vi.waitFor(() => expect(coordinator.clinePassSubscribed()).toBe(false)) + expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID) + apiConfig = { actModeApiProvider: "cline-pass", actModeClinePassModelId: "cline-pass/local" } + expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID) + await coordinator.dispose() + }) + it("keeps a local ClinePass model out of the cloud when the catalog doesn't offer ClinePass", async () => { const { coordinator } = makeCoordinator({ stateManager: { diff --git a/apps/vscode/src/sdk/sdk-cloud-session-coordinator.ts b/apps/vscode/src/sdk/sdk-cloud-session-coordinator.ts index 6e4110262d..58f75758db 100644 --- a/apps/vscode/src/sdk/sdk-cloud-session-coordinator.ts +++ b/apps/vscode/src/sdk/sdk-cloud-session-coordinator.ts @@ -902,12 +902,12 @@ export class SdkCloudSessionCoordinator { /** * The Cline model a new sandbox runs: the cloud model picked in Settings, - * else the user's Act-mode Cline or ClinePass model when the cloud catalog - * offers it, else the first ClinePass model for a subscriber, else Cline's - * default model. Until the catalog is known, a local Cline or ClinePass - * model is assumed to be offered. Synchronous, and the only source for both - * the composer label and the start path, so the sandbox runs the model the - * user saw when they submitted. + * else the user's Act-mode Cline model, or ClinePass model when the plan + * check confirms a current plan, if the cloud catalog offers it; else the + * first ClinePass model for a subscriber, else Cline's default model. Until + * the catalog is known, a local Cline or ClinePass model is assumed to be + * offered. Synchronous, and the only source for both the composer label and + * the start path, so the sandbox runs the model the user saw when they submitted. */ nextCloudModelId(): string { const chosen = this.options.stateManager.getGlobalStateKey("cloudModelId")?.trim() @@ -915,15 +915,16 @@ export class SdkCloudSessionCoordinator { return chosen } const apiConfig = this.options.stateManager.getApiConfiguration() + const account = this.accountCloudModels() + // The local ClinePass provider also serves free models without a subscription, + // so only a confirmed plan makes its model a ClinePass model to carry over. const local = apiConfig.actModeApiProvider === "cline" ? apiConfig.actModeClineModelId?.trim() - : apiConfig.actModeApiProvider === "cline-pass" + : apiConfig.actModeApiProvider === "cline-pass" && account?.subscribed ? apiConfig.actModeClinePassModelId?.trim() : undefined - const account = this.accountCloudModels() const catalog = account?.models - // The local ClinePass provider also offers free models, which the cloud does not take. const offered = catalog ? catalog.some((model) => model.id === local) : apiConfig.actModeApiProvider === "cline" || !!local?.startsWith("cline-pass/")