Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/opt-in-free-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Changed free model onboarding from automatic to opt-in: new users must click "Start with free models" button to enable free models instead of having them configured by default
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 @@ -948,6 +948,7 @@ export interface WebviewMessage {
| "openDebugUiHistory"
| "startDeviceAuth" // kilocode_change: Start device auth flow
| "cancelDeviceAuth" // kilocode_change: Cancel device auth flow
| "startWithFreeModels" // kilocode_change: Start with free models without auth
| "deviceAuthCompleteWithProfile" // kilocode_change: Device auth complete with specific profile
| "requestChatCompletion" // kilocode_change: Request FIM completion for chat text area
| "chatCompletionAccepted" // kilocode_change: User accepted a chat completion suggestion
Expand Down
30 changes: 6 additions & 24 deletions src/core/config/ProviderSettingsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,9 @@ export class ProviderSettingsManager {
modes.map((mode) => [mode.slug, this.defaultConfigId]),
)

// kilocode_change start: Anonymous kilocode onboarding - set default provider for new users
private readonly defaultProviderProfiles: ProviderProfiles = {
currentApiConfigName: "default",
apiConfigs: {
default: {
id: this.defaultConfigId,
apiProvider: "kilocode",
kilocodeModel: "minimax/minimax-m2.1:free",
},
},
apiConfigs: { default: { id: this.defaultConfigId } },
modeApiConfigs: this.defaultModeApiConfigs,
migrations: {
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
Expand All @@ -86,7 +79,6 @@ export class ProviderSettingsManager {
claudeCodeLegacySettingsMigrated: true, // Mark as migrated on fresh installs
},
}
// kilocode_change end

// kilocode_change start
private pendingDuplicateIdRepairReport: Record<string, string[]> | null = null
Expand Down Expand Up @@ -148,15 +140,10 @@ export class ProviderSettingsManager {
async init_runMigrations() {
try {
return await this.lock(async () => {
// kilocode_change start: Check if this is a new user (no stored config)
const storedContent = await this.context.secrets.get(this.secretsKey)
const isNewUser = !storedContent
// kilocode_change end

const providerProfiles = await this.loadFromContent(storedContent)
const providerProfiles = await this.load()

if (isNewUser) {
await this.store(providerProfiles)
if (!providerProfiles) {
await this.store(this.defaultProviderProfiles)
return
}

Expand Down Expand Up @@ -774,13 +761,9 @@ export class ProviderSettingsManager {
}

private async load(): Promise<ProviderProfiles> {
const content = await this.context.secrets.get(this.secretsKey)
return this.loadFromContent(content)
}

// kilocode_change start: Extract content parsing to avoid double-fetching in init_runMigrations
private loadFromContent(content: string | undefined): ProviderProfiles {
try {
const content = await this.context.secrets.get(this.secretsKey)

if (!content) {
return this.defaultProviderProfiles
}
Expand Down Expand Up @@ -819,7 +802,6 @@ export class ProviderSettingsManager {
throw new Error(`Failed to read provider profiles from secrets: ${error}`)
}
}
// kilocode_change end

/**
* Sanitizes a provider config by resetting invalid/removed apiProvider values.
Expand Down
24 changes: 6 additions & 18 deletions src/core/config/__tests__/ProviderSettingsManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ describe("ProviderSettingsManager", () => {
// then reinitializing, and spying on internals of said initialization.
// I'm not convinced this test coverage means very much, so this fix makes a complicated puzzle happen to fall in place
// Also this override resets itself, but fortunately no test required triple initialization...

// Wait for the first manager's initialization to complete, then clear mock calls
// This is needed because new users get the default kilocode config stored
await providerSettingsManager.initialize()
vi.clearAllMocks()

providerSettingsManager.initialize = async () => {
providerSettingsManager = new ProviderSettingsManager(mockContext)
await providerSettingsManager.initialize()
Expand All @@ -57,21 +51,15 @@ describe("ProviderSettingsManager", () => {
})

describe("initialize", () => {
// kilocode_change start: test updated to expect kilocode default profile for new users
it("should initialize kilocode default profile when secrets.get returns null", async () => {
it("should not write to storage when secrets.get returns null", async () => {
// Mock readConfig to return null
mockSecrets.get.mockResolvedValueOnce(null)

await providerSettingsManager.initialize()

// Should write to storage with default kilocode profile for new users
expect(mockSecrets.store).toHaveBeenCalled()
const calls = mockSecrets.store.mock.calls
const storedConfig = JSON.parse(calls[calls.length - 1][1])
expect(storedConfig.apiConfigs.default.apiProvider).toBe("kilocode")
expect(storedConfig.apiConfigs.default.kilocodeModel).toBe("minimax/minimax-m2.1:free")
// Should not write to storage because readConfig returns defaultConfig
expect(mockSecrets.store).not.toHaveBeenCalled()
})
// kilocode_change end

it("should not initialize config if it exists and migrations are complete", async () => {
mockSecrets.get.mockResolvedValue(
Expand Down Expand Up @@ -449,7 +437,7 @@ describe("ProviderSettingsManager", () => {
mockSecrets.get.mockRejectedValue(new Error("Storage failed"))

await expect(providerSettingsManager.initialize()).rejects.toThrow(
"Failed to initialize config: Error: Storage failed",
"Failed to initialize config: Error: Failed to read provider profiles from secrets: Error: Storage failed",
)
})
})
Expand Down Expand Up @@ -510,7 +498,7 @@ describe("ProviderSettingsManager", () => {
mockSecrets.get.mockRejectedValue(new Error("Read failed"))

await expect(providerSettingsManager.listConfig()).rejects.toThrow(
"Failed to list configs: Error: Read failed",
"Failed to list configs: Error: Failed to read provider profiles from secrets: Error: Read failed",
)
})
})
Expand Down Expand Up @@ -947,7 +935,7 @@ describe("ProviderSettingsManager", () => {
mockSecrets.get.mockRejectedValue(new Error("Storage failed"))

await expect(providerSettingsManager.hasConfig("test")).rejects.toThrow(
"Failed to check config existence: Error: Storage failed",
"Failed to check config existence: Error: Failed to read provider profiles from secrets: Error: Storage failed",
)
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { deviceAuthMessageHandler } from "../webviewMessageHandlerUtils"
import { ClineProvider } from "../../../webview/ClineProvider"
import { WebviewMessage } from "../../../../shared/WebviewMessage"

// Mock the buildApiHandler
vi.mock("../../../../api", () => ({
buildApiHandler: vi.fn().mockReturnValue({}),
}))

describe("deviceAuthMessageHandler", () => {
let mockProvider: Partial<ClineProvider>

beforeEach(() => {
mockProvider = {
getState: vi.fn().mockResolvedValue({
apiConfiguration: {},
currentApiConfigName: "default",
}),
upsertProviderProfile: vi.fn().mockResolvedValue(undefined),
getCurrentTask: vi.fn().mockReturnValue(null),
postMessageToWebview: vi.fn(),
log: vi.fn(),
}
})

describe("startWithFreeModels", () => {
it("should set up the profile with kilocode provider and free model", async () => {
const message: WebviewMessage = { type: "startWithFreeModels" }

const result = await deviceAuthMessageHandler(mockProvider as ClineProvider, message)

expect(result).toBe(true)
expect(mockProvider.upsertProviderProfile).toHaveBeenCalledWith("default", {
apiProvider: "kilocode",
kilocodeModel: "minimax/minimax-m2.1:free",
})
})

it("should navigate to chat tab after setting up free models", async () => {
const message: WebviewMessage = { type: "startWithFreeModels" }

await deviceAuthMessageHandler(mockProvider as ClineProvider, message)

expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "action",
action: "switchTab",
tab: "chat",
})
})

it("should preserve existing apiConfiguration when setting up free models", async () => {
mockProvider.getState = vi.fn().mockResolvedValue({
apiConfiguration: {
existingSetting: "value",
anotherSetting: 123,
},
currentApiConfigName: "my-profile",
})

const message: WebviewMessage = { type: "startWithFreeModels" }

await deviceAuthMessageHandler(mockProvider as ClineProvider, message)

expect(mockProvider.upsertProviderProfile).toHaveBeenCalledWith("my-profile", {
existingSetting: "value",
anotherSetting: 123,
apiProvider: "kilocode",
kilocodeModel: "minimax/minimax-m2.1:free",
})
})

it("should log error if setup fails", async () => {
const error = new Error("Test error")
mockProvider.upsertProviderProfile = vi.fn().mockRejectedValue(error)

const message: WebviewMessage = { type: "startWithFreeModels" }

const result = await deviceAuthMessageHandler(mockProvider as ClineProvider, message)

expect(result).toBe(true) // Still returns true even on error
expect(mockProvider.log).toHaveBeenCalledWith("Error setting up free models: Test error")
})
})
})
22 changes: 22 additions & 0 deletions src/core/kilocode/webview/webviewMessageHandlerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,28 @@ export const deviceAuthMessageHandler = async (provider: ClineProvider, message:
provider.cancelDeviceAuth()
return true
}
case "startWithFreeModels": {
// Set up kilocode provider with free model (no token required)
try {
const { currentApiConfigName = "default", apiConfiguration } = await provider.getState()
await provider.upsertProviderProfile(currentApiConfigName, {
...apiConfiguration,
apiProvider: "kilocode",
kilocodeModel: "minimax/minimax-m2.1:free",
})

// Navigate to chat tab after setup using action message
await provider.postMessageToWebview({
type: "action",
action: "switchTab",
tab: "chat",
})
} catch (error) {
provider.log(`Error setting up free models: ${error instanceof Error ? error.message : String(error)}`)
vscode.window.showErrorMessage("Failed to set up free models")
}
return true
}
case "deviceAuthCompleteWithProfile": {
// Save token to specific profile or current profile if no profile name provided
if (message.values?.token) {
Expand Down
1 change: 1 addition & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4605,6 +4605,7 @@ export const webviewMessageHandler = async (
// kilocode_change start - Device Auth handlers
case "startDeviceAuth":
case "cancelDeviceAuth":
case "startWithFreeModels":
case "deviceAuthCompleteWithProfile": {
await deviceAuthMessageHandler(provider, message)
break
Expand Down
7 changes: 7 additions & 0 deletions webview-ui/src/components/kilocode/common/KiloCodeAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ const KiloCodeAuth: React.FC<KiloCodeAuthProps> = ({ onManualConfigClick, onLogi
<div className="w-full flex flex-col gap-5">
<ButtonPrimary onClick={handleStartDeviceAuth}>{t("kilocode:welcome.ctaButton")}</ButtonPrimary>

<ButtonSecondary
onClick={() => {
vscode.postMessage({ type: "startWithFreeModels" })
}}>
{t("kilocode:welcome.startWithFreeModels")}
</ButtonSecondary>

{!!onManualConfigClick && (
<ButtonSecondary onClick={() => onManualConfigClick && onManualConfigClick()}>
{t("kilocode:welcome.manualModeButton")}
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/en/kilocode.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"introText2": "It works with the latest AI models like Claude Opus 4.5, Gemini 3 Pro, GPT-5.2, and 450+ more.",
"introText3": "Create a free account and get $20 in bonus credits when you top-up for the first time.",
"ctaButton": "Get Started",
"startWithFreeModels": "Start with free models",
"manualModeButton": "Use your own API key",
"alreadySignedUp": "Already signed up?",
"loginText": "Log in here"
Expand Down
Loading