Skip to content
Merged
11 changes: 11 additions & 0 deletions .changeset/gentle-plants-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"kilo-code": minor
---

Added a new device authorization flow for Kilo Gateway that makes it easier to connect your editor to your Kilo account. Instead of manually copying API tokens, you can now:

- Scan a QR code with your phone or click to open the authorization page in your browser
- Approve the connection from your browser
- Automatically get authenticated without copying any tokens

This streamlined workflow provides a more secure and user-friendly way to authenticate, similar to how you connect devices to services like Netflix or YouTube.
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export * from "./tool-params.js"
export * from "./type-fu.js"
export * from "./vscode.js"
export * from "./kilocode/kilocode.js"
export * from "./kilocode/device-auth.js" // kilocode_change
export * from "./kilocode/nativeFunctionCallingProviders.js"
export * from "./usage-tracker.js" // kilocode_change

Expand Down
51 changes: 51 additions & 0 deletions packages/types/src/kilocode/device-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod"

/**
* Device authorization response from initiate endpoint
*/
export const DeviceAuthInitiateResponseSchema = z.object({
/** Verification code to display to user */
code: z.string(),
/** URL for user to visit in browser */
verificationUrl: z.string(),
/** Time in seconds until code expires */
expiresIn: z.number(),
})

export type DeviceAuthInitiateResponse = z.infer<typeof DeviceAuthInitiateResponseSchema>

/**
* Device authorization poll response
*/
export const DeviceAuthPollResponseSchema = z.object({
/** Current status of the authorization */
status: z.enum(["pending", "approved", "denied", "expired"]),
/** API token (only present when approved) */
token: z.string().optional(),
/** User ID (only present when approved) */
userId: z.string().optional(),
/** User email (only present when approved) */
userEmail: z.string().optional(),
})

export type DeviceAuthPollResponse = z.infer<typeof DeviceAuthPollResponseSchema>

/**
* Device auth state for UI
*/
export interface DeviceAuthState {
/** Current status of the auth flow */
status: "idle" | "initiating" | "pending" | "polling" | "success" | "error" | "cancelled"
/** Verification code */
code?: string
/** URL to visit for verification */
verificationUrl?: string
/** Expiration time in seconds */
expiresIn?: number
/** Error message if failed */
error?: string
/** Time remaining in seconds */
timeRemaining?: number
/** User email when successful */
userEmail?: string
}
150 changes: 150 additions & 0 deletions src/core/kilocode/webview/deviceAuthHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as vscode from "vscode"
import { DeviceAuthService } from "../../../services/kilocode/DeviceAuthService"
import type { ExtensionMessage } from "../../../shared/ExtensionMessage"

/**
* Callbacks required by DeviceAuthHandler to communicate with the provider
*/
export interface DeviceAuthHandlerCallbacks {
postMessageToWebview: (message: ExtensionMessage) => Promise<void>
log: (message: string) => void
showInformationMessage: (message: string) => void
}

/**
* Handles device authorization flow for Kilo Code authentication
* This class encapsulates all device auth logic to keep ClineProvider clean
*/
export class DeviceAuthHandler {
private deviceAuthService?: DeviceAuthService
private callbacks: DeviceAuthHandlerCallbacks

constructor(callbacks: DeviceAuthHandlerCallbacks) {
this.callbacks = callbacks
}

/**
* Start the device authorization flow
*/
async startDeviceAuth(): Promise<void> {
try {
// Clean up any existing device auth service
if (this.deviceAuthService) {
this.deviceAuthService.dispose()
}

this.deviceAuthService = new DeviceAuthService()

// Set up event listeners
this.deviceAuthService.on("started", (data: any) => {
this.callbacks.postMessageToWebview({
type: "deviceAuthStarted",
deviceAuthCode: data.code,
deviceAuthVerificationUrl: data.verificationUrl,
deviceAuthExpiresIn: data.expiresIn,
})
// Open browser automatically
vscode.env.openExternal(vscode.Uri.parse(data.verificationUrl))
})

this.deviceAuthService.on("polling", (timeRemaining: any) => {
this.callbacks.postMessageToWebview({
type: "deviceAuthPolling",
deviceAuthTimeRemaining: timeRemaining,
})
})

this.deviceAuthService.on("success", async (token: any, userEmail: any) => {
this.callbacks.postMessageToWebview({
type: "deviceAuthComplete",
deviceAuthToken: token,
deviceAuthUserEmail: userEmail,
})

this.callbacks.showInformationMessage(
`Kilo Code successfully configured! Authenticated as ${userEmail}`,
)

// Clean up
this.deviceAuthService?.dispose()
this.deviceAuthService = undefined
})

this.deviceAuthService.on("denied", () => {
this.callbacks.postMessageToWebview({
type: "deviceAuthFailed",
deviceAuthError: "Authorization was denied",
})

this.deviceAuthService?.dispose()
this.deviceAuthService = undefined
})

this.deviceAuthService.on("expired", () => {
this.callbacks.postMessageToWebview({
type: "deviceAuthFailed",
deviceAuthError: "Authorization code expired. Please try again.",
})

this.deviceAuthService?.dispose()
this.deviceAuthService = undefined
})

this.deviceAuthService.on("error", (error: any) => {
this.callbacks.postMessageToWebview({
type: "deviceAuthFailed",
deviceAuthError: error.message,
})

this.deviceAuthService?.dispose()
this.deviceAuthService = undefined
})

this.deviceAuthService.on("cancelled", () => {
this.callbacks.postMessageToWebview({
type: "deviceAuthCancelled",
})
})

// Start the auth flow
await this.deviceAuthService.initiate()
} catch (error) {
this.callbacks.log(`Error starting device auth: ${error instanceof Error ? error.message : String(error)}`)

this.callbacks.postMessageToWebview({
type: "deviceAuthFailed",
deviceAuthError: error instanceof Error ? error.message : "Failed to start authentication",
})

this.deviceAuthService?.dispose()
this.deviceAuthService = undefined
}
}

/**
* Cancel the device authorization flow
*/
cancelDeviceAuth(): void {
if (this.deviceAuthService) {
this.deviceAuthService.cancel()
// Clean up the service after cancellation
// Use setTimeout to avoid disposing during event emission
setTimeout(() => {
if (this.deviceAuthService) {
this.deviceAuthService.dispose()
this.deviceAuthService = undefined
}
}, 0)
}
}

/**
* Clean up resources
*/
dispose(): void {
if (this.deviceAuthService) {
this.deviceAuthService.dispose()
this.deviceAuthService = undefined
}
}
}
56 changes: 56 additions & 0 deletions src/core/kilocode/webview/webviewMessageHandlerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,59 @@ export const editMessageHandler = async (provider: ClineProvider, message: Webvi
}
return
}

/**
* Handles device authentication webview messages
* Supports: startDeviceAuth, cancelDeviceAuth, deviceAuthCompleteWithProfile
*/
export const deviceAuthMessageHandler = async (provider: ClineProvider, message: WebviewMessage): Promise<boolean> => {
switch (message.type) {
case "startDeviceAuth": {
await provider.startDeviceAuth()
return true
}
case "cancelDeviceAuth": {
provider.cancelDeviceAuth()
return true
}
case "deviceAuthCompleteWithProfile": {
// Save token to specific profile or current profile if no profile name provided
if (message.values?.token) {
const profileName = message.text || undefined // Empty string becomes undefined
const token = message.values.token as string
try {
if (profileName) {
// Save to specified profile
const { ...profileConfig } = await provider.providerSettingsManager.getProfile({
name: profileName,
})
await provider.upsertProviderProfile(
profileName,
{
...profileConfig,
apiProvider: "kilocode",
kilocodeToken: token,
},
false, // Don't activate - just save
)
} else {
// Save to current profile (from welcome screen)
const { apiConfiguration, currentApiConfigName = "default" } = await provider.getState()
await provider.upsertProviderProfile(currentApiConfigName, {
...apiConfiguration,
apiProvider: "kilocode",
kilocodeToken: token,
})
}
} catch (error) {
provider.log(
`Error saving device auth token: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
return true
}
default:
return false
}
}
24 changes: 22 additions & 2 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import { getKiloCodeWrapperProperties } from "../../core/kilocode/wrapper"
import { getKilocodeConfig, KilocodeConfig } from "../../utils/kilo-config-file"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
import { kilo_execIfExtension } from "../../shared/kilocode/cli-sessions/extension/session-manager-utils"
import { DeviceAuthHandler } from "../kilocode/webview/deviceAuthHandler"

export type ClineProviderState = Awaited<ReturnType<ClineProvider["getState"]>>
// kilocode_change end
Expand Down Expand Up @@ -157,6 +158,7 @@ export class ClineProvider
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
private currentWorkspacePath: string | undefined
private autoPurgeScheduler?: any // kilocode_change - (Any) Prevent circular import
private deviceAuthHandler?: DeviceAuthHandler // kilocode_change - Device auth handler

private recentTasksCache?: string[]
private pendingOperations: Map<string, PendingEditOperation> = new Map()
Expand Down Expand Up @@ -682,7 +684,7 @@ export class ClineProvider
this.marketplaceManager?.cleanup()
this.customModesManager?.dispose()

// kilocode_change start - Stop auto-purge scheduler
// kilocode_change start - Stop auto-purge scheduler and device auth service
if (this.autoPurgeScheduler) {
this.autoPurgeScheduler.stop()
this.autoPurgeScheduler = undefined
Expand Down Expand Up @@ -1747,7 +1749,7 @@ ${prompt}
await this.upsertProviderProfile(profileName, newConfiguration)
}

// kilocode_change:
// kilocode_change start
async handleKiloCodeCallback(token: string) {
const kilocode: ProviderName = "kilocode"
let { apiConfiguration, currentApiConfigName = "default" } = await this.getState()
Expand All @@ -1767,6 +1769,24 @@ ${prompt}
})
}
}
// kilocode_change end

// kilocode_change start - Device Auth Flow
async startDeviceAuth() {
if (!this.deviceAuthHandler) {
this.deviceAuthHandler = new DeviceAuthHandler({
postMessageToWebview: (msg) => this.postMessageToWebview(msg),
log: (msg) => this.log(msg),
showInformationMessage: (msg) => vscode.window.showInformationMessage(msg),
})
}
await this.deviceAuthHandler.startDeviceAuth()
}

cancelDeviceAuth() {
this.deviceAuthHandler?.cancelDeviceAuth()
}
// kilocode_change end

// Task history

Expand Down
16 changes: 15 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ import { generateSystemPrompt } from "./generateSystemPrompt"
import { getCommand } from "../../utils/commands"
import { toggleWorkflow, toggleRule, createRuleFile, deleteRuleFile } from "./kilorules"
import { mermaidFixPrompt } from "../prompts/utilities/mermaid" // kilocode_change
import { editMessageHandler, fetchKilocodeNotificationsHandler } from "../kilocode/webview/webviewMessageHandlerUtils" // kilocode_change
// kilocode_change start
import {
editMessageHandler,
fetchKilocodeNotificationsHandler,
deviceAuthMessageHandler,
} from "../kilocode/webview/webviewMessageHandlerUtils"
// kilocode_change end

const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])

Expand Down Expand Up @@ -4121,6 +4127,14 @@ export const webviewMessageHandler = async (
break
}
// kilocode_change end
// kilocode_change start - Device Auth handlers
case "startDeviceAuth":
case "cancelDeviceAuth":
case "deviceAuthCompleteWithProfile": {
await deviceAuthMessageHandler(provider, message)
break
}
// kilocode_change end
default: {
// console.log(`Unhandled message type: ${message.type}`)
//
Expand Down
Loading