Răsfoiți Sursa

feat: device auth workflow

Catriel Müller 2 luni în urmă
părinte
comite
548f8eed44

+ 1 - 0
packages/types/src/index.ts

@@ -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
 

+ 45 - 0
packages/types/src/kilocode/device-auth.ts

@@ -0,0 +1,45 @@
+/**
+ * Device authorization response from initiate endpoint
+ */
+export interface DeviceAuthInitiateResponse {
+	/** Verification code to display to user */
+	code: string
+	/** URL for user to visit in browser */
+	verificationUrl: string
+	/** Time in seconds until code expires */
+	expiresIn: number
+}
+
+/**
+ * Device authorization poll response
+ */
+export interface DeviceAuthPollResponse {
+	/** Current status of the authorization */
+	status: "pending" | "approved" | "denied" | "expired"
+	/** API token (only present when approved) */
+	token?: string
+	/** User ID (only present when approved) */
+	userId?: string
+	/** User email (only present when approved) */
+	userEmail?: string
+}
+
+/**
+ * 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
+}

+ 125 - 2
src/core/webview/ClineProvider.ts

@@ -157,6 +157,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 deviceAuthService?: any // kilocode_change - Device auth service
 
 	private recentTasksCache?: string[]
 	private pendingOperations: Map<string, PendingEditOperation> = new Map()
@@ -682,11 +683,21 @@ 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
 		}
+<<<<<<< HEAD
+=======
+
+		if (this.deviceAuthService) {
+			this.deviceAuthService.dispose()
+			this.deviceAuthService = undefined
+		}
+
+		await kilo_destroySessionManager()
+>>>>>>> 5d5976df57 (feat: device auth workflow)
 		// kilocode_change end
 
 		this.log("Disposed all disposables")
@@ -1747,7 +1758,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()
@@ -1767,6 +1778,118 @@ ${prompt}
 			})
 		}
 	}
+	// kilocode_change end
+
+	// kilocode_change start - Device Auth Flow
+	async startDeviceAuth() {
+		try {
+			// Clean up any existing device auth service
+			if (this.deviceAuthService) {
+				this.deviceAuthService.dispose()
+			}
+
+			// Import DeviceAuthService
+			const { DeviceAuthService } = await import("../../services/kilocode/DeviceAuthService")
+			this.deviceAuthService = new DeviceAuthService()
+
+			// Set up event listeners
+			this.deviceAuthService.on("started", (data: any) => {
+				this.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.postMessageToWebview({
+					type: "deviceAuthPolling",
+					deviceAuthTimeRemaining: timeRemaining,
+				})
+			})
+
+			this.deviceAuthService.on("success", async (token: any, userEmail: any) => {
+				this.postMessageToWebview({
+					type: "deviceAuthComplete",
+					deviceAuthToken: token,
+					deviceAuthUserEmail: userEmail,
+				})
+
+				vscode.window.showInformationMessage(`Kilo Code successfully configured! Authenticated as ${userEmail}`)
+
+				// Clean up
+				this.deviceAuthService?.dispose()
+				this.deviceAuthService = undefined
+			})
+
+			this.deviceAuthService.on("denied", () => {
+				this.postMessageToWebview({
+					type: "deviceAuthFailed",
+					deviceAuthError: "Authorization was denied",
+				})
+
+				this.deviceAuthService?.dispose()
+				this.deviceAuthService = undefined
+			})
+
+			this.deviceAuthService.on("expired", () => {
+				this.postMessageToWebview({
+					type: "deviceAuthFailed",
+					deviceAuthError: "Authorization code expired. Please try again.",
+				})
+
+				this.deviceAuthService?.dispose()
+				this.deviceAuthService = undefined
+			})
+
+			this.deviceAuthService.on("error", (error: any) => {
+				this.postMessageToWebview({
+					type: "deviceAuthFailed",
+					deviceAuthError: error.message,
+				})
+
+				this.deviceAuthService?.dispose()
+				this.deviceAuthService = undefined
+			})
+
+			this.deviceAuthService.on("cancelled", () => {
+				this.postMessageToWebview({
+					type: "deviceAuthCancelled",
+				})
+			})
+
+			// Start the auth flow
+			await this.deviceAuthService.initiate()
+		} catch (error) {
+			this.log(`Error starting device auth: ${error instanceof Error ? error.message : String(error)}`)
+
+			this.postMessageToWebview({
+				type: "deviceAuthFailed",
+				deviceAuthError: error instanceof Error ? error.message : "Failed to start authentication",
+			})
+
+			this.deviceAuthService?.dispose()
+			this.deviceAuthService = undefined
+		}
+	}
+
+	cancelDeviceAuth() {
+		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)
+		}
+	}
+	// kilocode_change end
 
 	// Task history
 

+ 47 - 0
src/core/webview/webviewMessageHandler.ts

@@ -4121,6 +4121,53 @@ export const webviewMessageHandler = async (
 			break
 		}
 		// kilocode_change end
+		// kilocode_change start - Device Auth handlers
+		case "startDeviceAuth": {
+			await provider.startDeviceAuth()
+			break
+		}
+		case "cancelDeviceAuth": {
+			provider.cancelDeviceAuth()
+			break
+		}
+		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)}`,
+					)
+				}
+			}
+			break
+		}
+		// kilocode_change end
 		default: {
 			// console.log(`Unhandled message type: ${message.type}`)
 			//

+ 169 - 0
src/services/kilocode/DeviceAuthService.ts

@@ -0,0 +1,169 @@
+import EventEmitter from "events"
+import { getApiUrl } from "@roo-code/types"
+import type { DeviceAuthInitiateResponse, DeviceAuthPollResponse } from "@roo-code/types"
+
+const POLL_INTERVAL_MS = 3000
+
+export interface DeviceAuthServiceEvents {
+	started: [data: DeviceAuthInitiateResponse]
+	polling: [timeRemaining: number]
+	success: [token: string, userEmail: string]
+	denied: []
+	expired: []
+	error: [error: Error]
+	cancelled: []
+}
+
+/**
+ * Service for handling device authorization flow
+ */
+export class DeviceAuthService extends EventEmitter<DeviceAuthServiceEvents> {
+	private pollIntervalId?: NodeJS.Timeout
+	private startTime?: number
+	private expiresIn?: number
+	private code?: string
+	private aborted = false
+
+	/**
+	 * Initiate device authorization flow
+	 * @returns Device authorization details
+	 * @throws Error if initiation fails
+	 */
+	async initiate(): Promise<DeviceAuthInitiateResponse> {
+		try {
+			const response = await fetch(getApiUrl("/api/device-auth/codes"), {
+				method: "POST",
+				headers: {
+					"Content-Type": "application/json",
+				},
+			})
+
+			if (!response.ok) {
+				if (response.status === 429) {
+					throw new Error("Too many pending authorization requests. Please try again later.")
+				}
+				throw new Error(`Failed to initiate device authorization: ${response.status}`)
+			}
+
+			const data = (await response.json()) as DeviceAuthInitiateResponse
+
+			this.code = data.code
+			this.expiresIn = data.expiresIn
+			this.startTime = Date.now()
+			this.aborted = false
+
+			this.emit("started", data)
+
+			// Start polling
+			this.startPolling()
+
+			return data
+		} catch (error) {
+			const err = error instanceof Error ? error : new Error(String(error))
+			this.emit("error", err)
+			throw err
+		}
+	}
+
+	/**
+	 * Poll for device authorization status
+	 */
+	private async poll(): Promise<void> {
+		if (!this.code || this.aborted) {
+			return
+		}
+
+		try {
+			const response = await fetch(getApiUrl(`/api/device-auth/codes/${this.code}`))
+
+			// Guard against undefined response (can happen in tests or network errors)
+			if (!response) {
+				return
+			}
+
+			if (response.status === 202) {
+				// Still pending - emit time remaining
+				if (this.startTime && this.expiresIn) {
+					const elapsed = Math.floor((Date.now() - this.startTime) / 1000)
+					const remaining = Math.max(0, this.expiresIn - elapsed)
+					this.emit("polling", remaining)
+				}
+				return
+			}
+
+			// Stop polling for any non-pending status
+			this.stopPolling()
+
+			if (response.status === 403) {
+				// Denied by user
+				this.emit("denied")
+				return
+			}
+
+			if (response.status === 410) {
+				// Code expired
+				this.emit("expired")
+				return
+			}
+
+			if (!response.ok) {
+				throw new Error(`Failed to poll device authorization: ${response.status}`)
+			}
+
+			const data = (await response.json()) as DeviceAuthPollResponse
+
+			if (data.status === "approved" && data.token && data.userEmail) {
+				this.emit("success", data.token, data.userEmail)
+			} else if (data.status === "denied") {
+				this.emit("denied")
+			} else if (data.status === "expired") {
+				this.emit("expired")
+			}
+		} catch (error) {
+			this.stopPolling()
+			const err = error instanceof Error ? error : new Error(String(error))
+			this.emit("error", err)
+		}
+	}
+
+	/**
+	 * Start polling for authorization status
+	 */
+	private startPolling(): void {
+		this.stopPolling()
+		this.pollIntervalId = setInterval(() => {
+			this.poll()
+		}, POLL_INTERVAL_MS)
+
+		// Do first poll immediately
+		this.poll()
+	}
+
+	/**
+	 * Stop polling for authorization status
+	 */
+	private stopPolling(): void {
+		if (this.pollIntervalId) {
+			clearInterval(this.pollIntervalId)
+			this.pollIntervalId = undefined
+		}
+	}
+
+	/**
+	 * Cancel the device authorization flow
+	 */
+	cancel(): void {
+		this.aborted = true
+		this.stopPolling()
+		this.emit("cancelled")
+	}
+
+	/**
+	 * Clean up resources
+	 */
+	dispose(): void {
+		this.aborted = true
+		this.stopPolling()
+		this.removeAllListeners()
+	}
+}

+ 302 - 0
src/services/kilocode/__tests__/DeviceAuthService.test.ts

@@ -0,0 +1,302 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
+import { DeviceAuthService } from "../DeviceAuthService"
+import type { DeviceAuthInitiateResponse, DeviceAuthPollResponse } from "@roo-code/types"
+
+// Mock fetch globally
+global.fetch = vi.fn()
+
+describe("DeviceAuthService", () => {
+	let service: DeviceAuthService
+
+	beforeEach(() => {
+		service = new DeviceAuthService()
+		vi.clearAllMocks()
+		vi.useFakeTimers()
+	})
+
+	afterEach(() => {
+		service.dispose()
+		vi.useRealTimers()
+	})
+
+	describe("initiate", () => {
+		it("should successfully initiate device auth", async () => {
+			const mockResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			const startedSpy = vi.fn()
+			service.on("started", startedSpy)
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockResponse,
+			})
+
+			// Mock the first poll call to return pending
+			;(global.fetch as any).mockResolvedValueOnce({
+				status: 202,
+			})
+
+			const result = await service.initiate()
+
+			expect(result).toEqual(mockResponse)
+			expect(startedSpy).toHaveBeenCalledWith(mockResponse)
+			expect(global.fetch).toHaveBeenCalledWith(
+				expect.stringContaining("/api/device-auth/codes"),
+				expect.objectContaining({
+					method: "POST",
+					headers: {
+						"Content-Type": "application/json",
+					},
+				}),
+			)
+		})
+
+		it("should handle rate limiting (429)", async () => {
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: false,
+				status: 429,
+			})
+
+			const errorSpy = vi.fn()
+			service.on("error", errorSpy)
+
+			await expect(service.initiate()).rejects.toThrow("Too many pending authorization requests")
+			expect(errorSpy).toHaveBeenCalled()
+		})
+
+		it("should handle other errors", async () => {
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: false,
+				status: 500,
+			})
+
+			const errorSpy = vi.fn()
+			service.on("error", errorSpy)
+
+			await expect(service.initiate()).rejects.toThrow("Failed to initiate device authorization: 500")
+			expect(errorSpy).toHaveBeenCalled()
+		})
+	})
+
+	describe("polling", () => {
+		it("should emit polling event for pending status", async () => {
+			const pollingSpy = vi.fn()
+			service.on("polling", pollingSpy)
+
+			const mockInitResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			// Mock initiate call
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockInitResponse,
+			})
+
+			// Mock all subsequent polls to return pending to prevent infinite loop
+			;(global.fetch as any).mockResolvedValue({
+				status: 202,
+			})
+
+			await service.initiate()
+
+			// Wait for the immediate poll call
+			await vi.advanceTimersByTimeAsync(100)
+
+			expect(pollingSpy).toHaveBeenCalled()
+
+			// Clean up to prevent background timers
+			service.cancel()
+		})
+
+		it("should emit success event when approved", async () => {
+			const successSpy = vi.fn()
+			service.on("success", successSpy)
+
+			const mockInitResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			const mockPollResponse: DeviceAuthPollResponse = {
+				status: "approved",
+				token: "test-token",
+				userEmail: "[email protected]",
+			}
+
+			// Mock initiate call
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockInitResponse,
+			})
+
+			// Mock poll - approved
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				status: 200,
+				json: async () => mockPollResponse,
+			})
+
+			await service.initiate()
+
+			// Wait for the immediate poll call
+			await vi.runAllTimersAsync()
+
+			expect(successSpy).toHaveBeenCalledWith("test-token", "[email protected]")
+		})
+
+		it("should emit denied event when user denies", async () => {
+			const deniedSpy = vi.fn()
+			service.on("denied", deniedSpy)
+
+			const mockInitResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			// Mock initiate call
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockInitResponse,
+			})
+
+			// Mock poll - denied
+			;(global.fetch as any).mockResolvedValueOnce({
+				status: 403,
+			})
+
+			await service.initiate()
+
+			// Wait for the immediate poll call
+			await vi.runAllTimersAsync()
+
+			expect(deniedSpy).toHaveBeenCalled()
+		})
+
+		it("should emit expired event when code expires", async () => {
+			const expiredSpy = vi.fn()
+			service.on("expired", expiredSpy)
+
+			const mockInitResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			// Mock initiate call
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockInitResponse,
+			})
+
+			// Mock poll - expired
+			;(global.fetch as any).mockResolvedValueOnce({
+				status: 410,
+			})
+
+			await service.initiate()
+
+			// Wait for the immediate poll call
+			await vi.runAllTimersAsync()
+
+			expect(expiredSpy).toHaveBeenCalled()
+		})
+
+		it("should handle polling errors", async () => {
+			const errorSpy = vi.fn()
+			service.on("error", errorSpy)
+
+			const mockInitResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			// Mock initiate call
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockInitResponse,
+			})
+
+			// Mock poll - error
+			;(global.fetch as any).mockRejectedValueOnce(new Error("Network error"))
+
+			await service.initiate()
+
+			// Wait for the immediate poll call
+			await vi.runAllTimersAsync()
+
+			expect(errorSpy).toHaveBeenCalled()
+		})
+	})
+
+	describe("cancel", () => {
+		it("should emit cancelled event and stop polling", async () => {
+			const cancelledSpy = vi.fn()
+			service.on("cancelled", cancelledSpy)
+
+			const mockResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockResponse,
+			})
+
+			// Mock first poll
+			;(global.fetch as any).mockResolvedValueOnce({
+				status: 202,
+			})
+
+			await service.initiate()
+
+			service.cancel()
+
+			expect(cancelledSpy).toHaveBeenCalled()
+
+			// Verify polling stopped by checking no more fetch calls after cancel
+			vi.clearAllMocks()
+			await vi.advanceTimersByTimeAsync(5000)
+			expect(global.fetch).not.toHaveBeenCalled()
+		})
+	})
+
+	describe("dispose", () => {
+		it("should clean up resources", async () => {
+			const mockResponse: DeviceAuthInitiateResponse = {
+				code: "ABC123",
+				verificationUrl: "https://kilo.ai/device/verify",
+				expiresIn: 600,
+			}
+
+			;(global.fetch as any).mockResolvedValueOnce({
+				ok: true,
+				json: async () => mockResponse,
+			})
+
+			// Mock first poll
+			;(global.fetch as any).mockResolvedValueOnce({
+				status: 202,
+			})
+
+			await service.initiate()
+
+			service.dispose()
+
+			// Verify polling stopped
+			vi.clearAllMocks()
+			await vi.advanceTimersByTimeAsync(5000)
+			expect(global.fetch).not.toHaveBeenCalled()
+		})
+	})
+})

+ 14 - 1
src/shared/ExtensionMessage.ts

@@ -184,7 +184,11 @@ export interface ExtensionMessage {
 		| "taskMetadataSaved" // kilocode_change: File save event for task metadata
 		| "managedIndexerState" // kilocode_change
 		| "singleCompletionResult" // kilocode_change
-		| "managedIndexerState" // kilocode_change
+		| "deviceAuthStarted" // kilocode_change: Device auth initiated
+		| "deviceAuthPolling" // kilocode_change: Device auth polling update
+		| "deviceAuthComplete" // kilocode_change: Device auth successful
+		| "deviceAuthFailed" // kilocode_change: Device auth failed
+		| "deviceAuthCancelled" // kilocode_change: Device auth cancelled
 	text?: string
 	// kilocode_change start
 	completionRequestId?: string // Correlation ID from request
@@ -335,6 +339,15 @@ export interface ExtensionMessage {
 	browserSessionMessages?: ClineMessage[] // For browser session panel updates
 	isBrowserSessionActive?: boolean // For browser session panel updates
 	stepIndex?: number // For browserSessionNavigate: the target step index to display
+	// kilocode_change start: Device auth data
+	deviceAuthCode?: string
+	deviceAuthVerificationUrl?: string
+	deviceAuthExpiresIn?: number
+	deviceAuthTimeRemaining?: number
+	deviceAuthToken?: string
+	deviceAuthUserEmail?: string
+	deviceAuthError?: string
+	// kilocode_change end: Device auth data
 }
 
 export type ExtensionState = Pick<

+ 4 - 1
src/shared/WebviewMessage.ts

@@ -270,11 +270,14 @@ export interface WebviewMessage {
 		| "sessionFork" // kilocode_change
 		| "sessionShow" // kilocode_change
 		| "singleCompletion" // kilocode_change
+		| "startDeviceAuth" // kilocode_change: Start device auth flow
+		| "cancelDeviceAuth" // kilocode_change: Cancel device auth flow
+		| "deviceAuthCompleteWithProfile" // kilocode_change: Device auth complete with specific profile
 	text?: string
 	completionRequestId?: string // kilocode_change
 	shareId?: string // kilocode_change - for sessionFork
 	editedMessageContent?: string
-	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
+	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" | "auth" // kilocode_change
 	disabled?: boolean
 	context?: string
 	dataUri?: string

+ 48 - 4
webview-ui/src/App.tsx

@@ -17,6 +17,7 @@ import HistoryView from "./components/history/HistoryView"
 import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView"
 import WelcomeView from "./components/kilocode/welcome/WelcomeView" // kilocode_change
 import ProfileView from "./components/kilocode/profile/ProfileView" // kilocode_change
+import AuthView from "./components/kilocode/auth/AuthView" // kilocode_change
 import McpView from "./components/mcp/McpView"
 import { MarketplaceView } from "./components/marketplace/MarketplaceView"
 import ModesView from "./components/modes/ModesView"
@@ -34,7 +35,7 @@ import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
 import { useKiloIdentity } from "./utils/kilocode/useKiloIdentity"
 import { MemoryWarningBanner } from "./kilocode/MemoryWarningBanner"
 
-type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" | "cloud" | "profile" // kilocode_change: add "profile"
+type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" | "cloud" | "profile" | "auth" // kilocode_change: add "profile" and "auth"
 
 interface HumanRelayDialogState {
 	isOpen: boolean
@@ -114,6 +115,9 @@ const App = () => {
 
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
 	const [tab, setTab] = useState<Tab>("chat")
+	const [authReturnTo, setAuthReturnTo] = useState<"chat" | "settings">("chat")
+	const [authProfileName, setAuthProfileName] = useState<string | undefined>(undefined)
+	const [settingsEditingProfile, setSettingsEditingProfile] = useState<string | undefined>(undefined)
 
 	const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
 		isOpen: false,
@@ -151,7 +155,11 @@ const App = () => {
 			setCurrentSection(undefined)
 			setCurrentMarketplaceTab(undefined)
 
-			if (settingsRef.current?.checkUnsaveChanges) {
+			// kilocode_change: strart - Bypass unsaved changes check when navigating to auth tab
+			if (newTab === "auth") {
+				setTab(newTab)
+			} else if (settingsRef.current?.checkUnsaveChanges) {
+				// kilocode_change: end
 				settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
 			} else {
 				setTab(newTab)
@@ -181,6 +189,19 @@ const App = () => {
 				// Handle switchTab action with tab parameter
 				if (message.action === "switchTab" && message.tab) {
 					const targetTab = message.tab as Tab
+					// kilocode_change start - Handle auth tab with returnTo and profileName parameters
+					if (targetTab === "auth") {
+						if (message.values?.returnTo) {
+							const returnTo = message.values.returnTo as "chat" | "settings"
+							setAuthReturnTo(returnTo)
+						}
+						if (message.values?.profileName) {
+							const profileName = message.values.profileName as string
+							setAuthProfileName(profileName)
+							setSettingsEditingProfile(profileName)
+						}
+					}
+					// kilocode_change end
 					switchTab(targetTab)
 					// Extract targetSection from values if provided
 					const targetSection = message.values?.section as string | undefined
@@ -191,11 +212,27 @@ const App = () => {
 					const newTab = tabsByMessageAction[message.action]
 					const section = message.values?.section as string | undefined
 					const marketplaceTab = message.values?.marketplaceTab as string | undefined
+					const editingProfile = message.values?.editingProfile as string | undefined // kilocode_change
 
 					if (newTab) {
 						switchTab(newTab)
 						setCurrentSection(section)
 						setCurrentMarketplaceTab(marketplaceTab)
+						// kilocode_change start - If navigating to settings with editingProfile, forward it
+						if (newTab === "settings" && editingProfile) {
+							// Re-send the message to SettingsView with the editingProfile
+							setTimeout(() => {
+								window.postMessage(
+									{
+										type: "action",
+										action: "settingsButtonClicked",
+										values: { editingProfile },
+									},
+									"*",
+								)
+							}, 100)
+						}
+						// kilocode_change end
 					}
 				}
 			}
@@ -304,11 +341,18 @@ const App = () => {
 			{tab === "modes" && <ModesView onDone={() => switchTab("chat")} />}
 			{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
 			{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
+			{/* kilocode_change: auth redirect / editingProfile */}
 			{tab === "settings" && (
-				<SettingsView ref={settingsRef} onDone={() => switchTab("chat")} targetSection={currentSection} /> // kilocode_change
+				<SettingsView
+					ref={settingsRef}
+					onDone={() => switchTab("chat")}
+					targetSection={currentSection}
+					editingProfile={settingsEditingProfile}
+				/>
 			)}
-			{/* kilocode_change: add profileview */}
+			{/* kilocode_change: add profileview and authview */}
 			{tab === "profile" && <ProfileView onDone={() => switchTab("chat")} />}
+			{tab === "auth" && <AuthView returnTo={authReturnTo} profileName={authProfileName} />}
 			{tab === "marketplace" && (
 				<MarketplaceView
 					stateManager={marketplaceStateManager}

+ 129 - 0
webview-ui/src/components/kilocode/auth/AuthView.tsx

@@ -0,0 +1,129 @@
+import { useEffect, useState } from "react"
+import { vscode } from "@/utils/vscode"
+import { Tab, TabContent } from "../../common/Tab"
+import DeviceAuthCard from "../common/DeviceAuthCard"
+
+interface AuthViewProps {
+	returnTo?: "chat" | "settings"
+	profileName?: string
+}
+
+type DeviceAuthStatus = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled"
+
+const AuthView: React.FC<AuthViewProps> = ({ returnTo = "chat", profileName }) => {
+	const [deviceAuthStatus, setDeviceAuthStatus] = useState<DeviceAuthStatus>("idle")
+	const [deviceAuthCode, setDeviceAuthCode] = useState<string>()
+	const [deviceAuthVerificationUrl, setDeviceAuthVerificationUrl] = useState<string>()
+	const [deviceAuthExpiresIn, setDeviceAuthExpiresIn] = useState<number>()
+	const [deviceAuthTimeRemaining, setDeviceAuthTimeRemaining] = useState<number>()
+	const [deviceAuthError, setDeviceAuthError] = useState<string>()
+	const [deviceAuthUserEmail, setDeviceAuthUserEmail] = useState<string>()
+
+	// Listen for device auth messages from extension
+	useEffect(() => {
+		const handleMessage = (event: MessageEvent) => {
+			const message = event.data
+			switch (message.type) {
+				case "deviceAuthStarted":
+					setDeviceAuthStatus("pending")
+					setDeviceAuthCode(message.deviceAuthCode)
+					setDeviceAuthVerificationUrl(message.deviceAuthVerificationUrl)
+					setDeviceAuthExpiresIn(message.deviceAuthExpiresIn)
+					setDeviceAuthTimeRemaining(message.deviceAuthExpiresIn)
+					setDeviceAuthError(undefined)
+					break
+				case "deviceAuthPolling":
+					setDeviceAuthTimeRemaining(message.deviceAuthTimeRemaining)
+					break
+				case "deviceAuthComplete":
+					console.log("[AuthView] Device auth complete received", {
+						profileName,
+						token: message.deviceAuthToken ? "present" : "missing",
+						userEmail: message.deviceAuthUserEmail,
+					})
+					setDeviceAuthStatus("success")
+					setDeviceAuthUserEmail(message.deviceAuthUserEmail)
+
+					// Always send profile-specific message to prevent double-save
+					// If no profileName, backend will use current profile
+					console.log(
+						"[AuthView] Sending deviceAuthCompleteWithProfile to profile:",
+						profileName || "current",
+					)
+					vscode.postMessage({
+						type: "deviceAuthCompleteWithProfile",
+						text: profileName || "", // Empty string means use current profile
+						values: {
+							token: message.deviceAuthToken,
+							userEmail: message.deviceAuthUserEmail,
+						},
+					})
+
+					// Navigate back after 2 seconds
+					setTimeout(() => {
+						vscode.postMessage({
+							type: "switchTab",
+							tab: returnTo,
+							values: profileName ? { editingProfile: profileName } : undefined,
+						})
+					}, 2000)
+					break
+				case "deviceAuthFailed":
+					setDeviceAuthStatus("error")
+					setDeviceAuthError(message.deviceAuthError)
+					break
+				case "deviceAuthCancelled":
+					// Navigate back immediately on cancel
+					vscode.postMessage({
+						type: "switchTab",
+						tab: returnTo,
+						values: profileName ? { editingProfile: profileName } : undefined,
+					})
+					break
+			}
+		}
+
+		window.addEventListener("message", handleMessage)
+		return () => window.removeEventListener("message", handleMessage)
+	}, [returnTo, profileName])
+
+	// Auto-start device auth when component mounts
+	useEffect(() => {
+		setDeviceAuthStatus("initiating")
+		vscode.postMessage({ type: "startDeviceAuth" })
+	}, [])
+
+	const handleCancelDeviceAuth = () => {
+		// Navigation will be handled by deviceAuthCancelled message
+	}
+
+	const handleRetryDeviceAuth = () => {
+		setDeviceAuthStatus("idle")
+		setDeviceAuthError(undefined)
+		// Automatically start again
+		setTimeout(() => {
+			setDeviceAuthStatus("initiating")
+			vscode.postMessage({ type: "startDeviceAuth" })
+		}, 100)
+	}
+
+	return (
+		<Tab>
+			<TabContent className="flex flex-col items-center justify-center min-h-screen p-6">
+				<DeviceAuthCard
+					code={deviceAuthCode}
+					verificationUrl={deviceAuthVerificationUrl}
+					expiresIn={deviceAuthExpiresIn}
+					timeRemaining={deviceAuthTimeRemaining}
+					status={deviceAuthStatus}
+					error={deviceAuthError}
+					userEmail={deviceAuthUserEmail}
+					onCancel={handleCancelDeviceAuth}
+					onRetry={handleRetryDeviceAuth}
+				/>
+			</TabContent>
+		</Tab>
+	)
+}
+
+export default AuthView

+ 215 - 0
webview-ui/src/components/kilocode/common/DeviceAuthCard.tsx

@@ -0,0 +1,215 @@
+import React, { useEffect, useState } from "react"
+import { useAppTranslation } from "@/i18n/TranslationContext"
+import { generateQRCode } from "@/utils/kilocode/qrcode"
+import { ButtonPrimary } from "./ButtonPrimary"
+import { ButtonSecondary } from "./ButtonSecondary"
+import { vscode } from "@/utils/vscode"
+import Logo from "./Logo"
+
+interface DeviceAuthCardProps {
+	code?: string
+	verificationUrl?: string
+	expiresIn?: number
+	timeRemaining?: number
+	status: "idle" | "initiating" | "pending" | "success" | "error" | "cancelled"
+	error?: string
+	userEmail?: string
+	onCancel?: () => void
+	onRetry?: () => void
+}
+
+const DeviceAuthCard: React.FC<DeviceAuthCardProps> = ({
+	code,
+	verificationUrl,
+	timeRemaining,
+	status,
+	error,
+	userEmail,
+	onCancel,
+	onRetry,
+}) => {
+	const { t } = useAppTranslation()
+	const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string>("")
+
+	// Generate QR code when verification URL is available
+	useEffect(() => {
+		if (verificationUrl) {
+			generateQRCode(verificationUrl, {
+				width: 200,
+				margin: 2,
+			})
+				.then(setQrCodeDataUrl)
+				.catch((err) => {
+					console.error("Failed to generate QR code:", err)
+				})
+		}
+	}, [verificationUrl])
+
+	// Format time remaining as MM:SS
+	const formatTime = (seconds?: number): string => {
+		if (seconds === undefined) return "--:--"
+		const mins = Math.floor(seconds / 60)
+		const secs = seconds % 60
+		return `${mins}:${secs.toString().padStart(2, "0")}`
+	}
+
+	const handleOpenBrowser = () => {
+		if (verificationUrl) {
+			vscode.postMessage({ type: "openExternal", url: verificationUrl })
+		}
+	}
+
+	const handleCancel = () => {
+		vscode.postMessage({ type: "cancelDeviceAuth" })
+		onCancel?.()
+	}
+	const handleRetry = () => {
+		onRetry?.()
+	}
+
+	// Render different states
+	if (status === "initiating") {
+		return (
+			<div className="flex flex-col items-center gap-4 p-6 bg-vscode-sideBar-background rounded">
+				<Logo />
+				<div className="flex items-center gap-2">
+					<span className="codicon codicon-loading codicon-modifier-spin text-xl"></span>
+					<span className="text-vscode-foreground">{t("kilocode:deviceAuth.initiating")}</span>
+				</div>
+			</div>
+		)
+	}
+
+	if (status === "success") {
+		return (
+			<div className="flex flex-col items-center gap-4 p-6 bg-vscode-sideBar-background rounded">
+				<Logo />
+				<h3 className="text-lg font-semibold text-vscode-foreground">{t("kilocode:deviceAuth.success")}</h3>
+				{userEmail && (
+					<p className="text-sm text-vscode-descriptionForeground">
+						{t("kilocode:deviceAuth.authenticatedAs", { email: userEmail })}
+					</p>
+				)}
+			</div>
+		)
+	}
+
+	if (status === "error") {
+		return (
+			<div className="flex flex-col items-center gap-4 p-6 bg-vscode-sideBar-background rounded">
+				<Logo />
+				<h3 className="text-lg font-semibold text-vscode-foreground">{t("kilocode:deviceAuth.error")}</h3>
+				<p className="text-sm text-vscode-descriptionForeground text-center">
+					{error || t("kilocode:deviceAuth.unknownError")}
+				</p>
+				<ButtonPrimary onClick={handleRetry}>{t("kilocode:deviceAuth.retry")}</ButtonPrimary>
+			</div>
+		)
+	}
+
+	if (status === "cancelled") {
+		return (
+			<div className="flex flex-col items-center gap-4 p-6 bg-vscode-sideBar-background rounded">
+				<Logo />
+				<h3 className="text-lg font-semibold text-vscode-foreground">{t("kilocode:deviceAuth.cancelled")}</h3>
+				<ButtonPrimary onClick={handleRetry}>{t("kilocode:deviceAuth.tryAgain")}</ButtonPrimary>
+			</div>
+		)
+	}
+
+	// Pending state - show code and QR
+	if (status === "pending" && code && verificationUrl) {
+		const handleCopyUrl = () => {
+			navigator.clipboard.writeText(verificationUrl)
+		}
+
+		return (
+			<div className="flex flex-col gap-2 bg-vscode-sideBar-background rounded">
+				<h3 className="text-lg font-semibold text-vscode-foreground text-center">
+					{t("kilocode:deviceAuth.title")}
+				</h3>
+
+				{/* Step 1: URL Section */}
+				<div className="flex flex-col gap-3">
+					<p className="text-sm text-vscode-descriptionForeground text-center">
+						{t("kilocode:deviceAuth.step1")}
+					</p>
+
+					{/* URL Box with Copy and Open Browser */}
+					<div className="flex flex-col gap-2">
+						<div className="flex items-center gap-2 p-3 bg-vscode-input-background border border-vscode-input-border rounded">
+							<span className="flex-1 text-sm font-mono text-vscode-foreground truncate">
+								{verificationUrl}
+							</span>
+							<button
+								onClick={handleCopyUrl}
+								className="flex-shrink-0 p-1 hover:bg-vscode-toolbar-hoverBackground rounded"
+								title={t("kilocode:deviceAuth.copyUrl")}>
+								<span className="codicon codicon-copy text-vscode-foreground"></span>
+							</button>
+						</div>
+						<ButtonPrimary onClick={handleOpenBrowser}>
+							{t("kilocode:deviceAuth.openBrowser")}
+						</ButtonPrimary>
+					</div>
+
+					{/* QR Code Section */}
+					{qrCodeDataUrl && (
+						<div className="flex flex-col items-center gap-2 mt-2">
+							<p className="text-sm text-vscode-descriptionForeground">
+								{t("kilocode:deviceAuth.scanQr")}
+							</p>
+							<img
+								src={qrCodeDataUrl}
+								alt="QR Code"
+								className="w-40 h-40 border border-vscode-widget-border rounded"
+							/>
+						</div>
+					)}
+				</div>
+
+				{/* Step 2: Verification Section */}
+				<div className="flex flex-col gap-3">
+					<p className="text-sm text-vscode-descriptionForeground text-center">
+						{t("kilocode:deviceAuth.step2")}
+					</p>
+
+					{/* Verification Code */}
+					<div className="flex justify-center">
+						<div className="px-6 py-3 bg-vscode-input-background border-2 border-vscode-focusBorder rounded-lg">
+							<span className="text-2xl font-mono font-bold text-vscode-foreground tracking-wider">
+								{code}
+							</span>
+						</div>
+					</div>
+
+					{/* Time Remaining */}
+					<div className="flex items-center justify-center gap-2">
+						<span className="codicon codicon-clock text-vscode-descriptionForeground"></span>
+						<span className="text-sm text-vscode-descriptionForeground">
+							{t("kilocode:deviceAuth.timeRemaining", { time: formatTime(timeRemaining) })}
+						</span>
+					</div>
+
+					{/* Status */}
+					<div className="flex items-center justify-center gap-2">
+						<span className="codicon codicon-loading codicon-modifier-spin text-vscode-descriptionForeground"></span>
+						<span className="text-sm text-vscode-descriptionForeground">
+							{t("kilocode:deviceAuth.waiting")}
+						</span>
+					</div>
+				</div>
+
+				{/* Cancel Button */}
+				<div className="w-full flex flex-col">
+					<ButtonSecondary onClick={handleCancel}>{t("kilocode:deviceAuth.cancel")}</ButtonSecondary>
+				</div>
+			</div>
+		)
+	}
+
+	// Idle state - shouldn't normally be shown
+	return null
+}
+
+export default DeviceAuthCard

+ 117 - 15
webview-ui/src/components/kilocode/common/KiloCodeAuth.tsx

@@ -1,21 +1,131 @@
-import React from "react"
-import { ButtonLink } from "./ButtonLink"
+import React, { useEffect, useState } from "react"
 import { ButtonSecondary } from "./ButtonSecondary"
+import { ButtonPrimary } from "./ButtonPrimary"
 import Logo from "./Logo"
 import { useAppTranslation } from "@/i18n/TranslationContext"
-import { getKiloCodeBackendSignUpUrl } from "../helpers"
-import { useExtensionState } from "@/context/ExtensionStateContext"
+import { vscode } from "@/utils/vscode"
+import DeviceAuthCard from "./DeviceAuthCard"
 
 interface KiloCodeAuthProps {
 	onManualConfigClick?: () => void
+	onLoginClick?: () => void
 	className?: string
 }
 
-const KiloCodeAuth: React.FC<KiloCodeAuthProps> = ({ onManualConfigClick, className = "" }) => {
-	const { uriScheme, uiKind, kiloCodeWrapperProperties } = useExtensionState()
+type DeviceAuthStatus = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled"
 
+const KiloCodeAuth: React.FC<KiloCodeAuthProps> = ({ onManualConfigClick, onLoginClick, className = "" }) => {
 	const { t } = useAppTranslation()
+	const [deviceAuthStatus, setDeviceAuthStatus] = useState<DeviceAuthStatus>("idle")
+	const [deviceAuthCode, setDeviceAuthCode] = useState<string>()
+	const [deviceAuthVerificationUrl, setDeviceAuthVerificationUrl] = useState<string>()
+	const [deviceAuthExpiresIn, setDeviceAuthExpiresIn] = useState<number>()
+	const [deviceAuthTimeRemaining, setDeviceAuthTimeRemaining] = useState<number>()
+	const [deviceAuthError, setDeviceAuthError] = useState<string>()
+	const [deviceAuthUserEmail, setDeviceAuthUserEmail] = useState<string>()
 
+	// Listen for device auth messages from extension
+	useEffect(() => {
+		const handleMessage = (event: MessageEvent) => {
+			const message = event.data
+			switch (message.type) {
+				case "deviceAuthStarted":
+					setDeviceAuthStatus("pending")
+					setDeviceAuthCode(message.deviceAuthCode)
+					setDeviceAuthVerificationUrl(message.deviceAuthVerificationUrl)
+					setDeviceAuthExpiresIn(message.deviceAuthExpiresIn)
+					setDeviceAuthTimeRemaining(message.deviceAuthExpiresIn)
+					setDeviceAuthError(undefined)
+					break
+				case "deviceAuthPolling":
+					setDeviceAuthTimeRemaining(message.deviceAuthTimeRemaining)
+					break
+				case "deviceAuthComplete":
+					setDeviceAuthStatus("success")
+					setDeviceAuthUserEmail(message.deviceAuthUserEmail)
+
+					// Save token to current profile
+					vscode.postMessage({
+						type: "deviceAuthCompleteWithProfile",
+						text: "", // Empty string means use current profile
+						values: {
+							token: message.deviceAuthToken,
+							userEmail: message.deviceAuthUserEmail,
+						},
+					})
+
+					// Navigate to chat tab after 2 seconds
+					setTimeout(() => {
+						vscode.postMessage({
+							type: "switchTab",
+							tab: "chat",
+						})
+					}, 2000)
+					break
+				case "deviceAuthFailed":
+					setDeviceAuthStatus("error")
+					setDeviceAuthError(message.deviceAuthError)
+					break
+				case "deviceAuthCancelled":
+					setDeviceAuthStatus("idle")
+					setDeviceAuthCode(undefined)
+					setDeviceAuthVerificationUrl(undefined)
+					setDeviceAuthExpiresIn(undefined)
+					setDeviceAuthTimeRemaining(undefined)
+					setDeviceAuthError(undefined)
+					break
+			}
+		}
+
+		window.addEventListener("message", handleMessage)
+		return () => window.removeEventListener("message", handleMessage)
+	}, [])
+
+	const handleStartDeviceAuth = () => {
+		if (onLoginClick) {
+			onLoginClick()
+		} else {
+			setDeviceAuthStatus("initiating")
+			vscode.postMessage({ type: "startDeviceAuth" })
+		}
+	}
+
+	const handleCancelDeviceAuth = () => {
+		setDeviceAuthStatus("idle")
+		setDeviceAuthCode(undefined)
+		setDeviceAuthVerificationUrl(undefined)
+		setDeviceAuthExpiresIn(undefined)
+		setDeviceAuthTimeRemaining(undefined)
+		setDeviceAuthError(undefined)
+	}
+
+	const handleRetryDeviceAuth = () => {
+		setDeviceAuthStatus("idle")
+		setDeviceAuthError(undefined)
+		// Automatically start again
+		setTimeout(() => handleStartDeviceAuth(), 100)
+	}
+
+	// Show device auth card if auth is in progress
+	if (deviceAuthStatus !== "idle") {
+		return (
+			<div className={`flex flex-col items-center ${className}`}>
+				<DeviceAuthCard
+					code={deviceAuthCode}
+					verificationUrl={deviceAuthVerificationUrl}
+					expiresIn={deviceAuthExpiresIn}
+					timeRemaining={deviceAuthTimeRemaining}
+					status={deviceAuthStatus}
+					error={deviceAuthError}
+					userEmail={deviceAuthUserEmail}
+					onCancel={handleCancelDeviceAuth}
+					onRetry={handleRetryDeviceAuth}
+				/>
+			</div>
+		)
+	}
+
+	// Default welcome screen
 	return (
 		<div className={`flex flex-col items-center ${className}`}>
 			<Logo />
@@ -26,15 +136,7 @@ const KiloCodeAuth: React.FC<KiloCodeAuthProps> = ({ onManualConfigClick, classN
 			<p className="text-center mb-5">{t("kilocode:welcome.introText3")}</p>
 
 			<div className="w-full flex flex-col gap-5">
-				<ButtonLink
-					href={getKiloCodeBackendSignUpUrl(uriScheme, uiKind, kiloCodeWrapperProperties)}
-					onClick={() => {
-						if (uiKind === "Web" && onManualConfigClick) {
-							onManualConfigClick()
-						}
-					}}>
-					{t("kilocode:welcome.ctaButton")}
-				</ButtonLink>
+				<ButtonPrimary onClick={handleStartDeviceAuth}>{t("kilocode:welcome.ctaButton")}</ButtonPrimary>
 
 				{!!onManualConfigClick && (
 					<ButtonSecondary onClick={() => onManualConfigClick && onManualConfigClick()}>

+ 9 - 12
webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx

@@ -1,16 +1,13 @@
 import { useCallback } from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { getKiloCodeBackendSignInUrl } from "../../helpers"
 import { Button } from "@src/components/ui"
 import { type ProviderSettings, type OrganizationAllowList } from "@roo-code/types"
 import type { RouterModels } from "@roo/api"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
 import { inputEventTransform } from "../../../settings/transforms"
 import { ModelPicker } from "../../../settings/ModelPicker"
 import { vscode } from "@src/utils/vscode"
 import { OrganizationSelector } from "../../common/OrganizationSelector"
-import { KiloCodeWrapperProperties } from "../../../../../../src/shared/kilocode/wrapper"
 import { getAppUrl } from "@roo-code/types"
 import { useKiloIdentity } from "@src/utils/kilocode/useKiloIdentity"
 
@@ -21,9 +18,6 @@ type KiloCodeProps = {
 	hideKiloCodeButton?: boolean
 	routerModels?: RouterModels
 	organizationAllowList: OrganizationAllowList
-	uriScheme: string | undefined
-	kiloCodeWrapperProperties: KiloCodeWrapperProperties | undefined
-	uiKind: string | undefined
 	kilocodeDefaultModel: string
 }
 
@@ -34,9 +28,6 @@ export const KiloCode = ({
 	hideKiloCodeButton,
 	routerModels,
 	organizationAllowList,
-	uriScheme,
-	uiKind,
-	kiloCodeWrapperProperties,
 	kilocodeDefaultModel,
 }: KiloCodeProps) => {
 	const { t } = useAppTranslation()
@@ -92,11 +83,17 @@ export const KiloCode = ({
 						</Button>
 					</div>
 				) : (
-					<VSCodeButtonLink
+					<Button
 						variant="secondary"
-						href={getKiloCodeBackendSignInUrl(uriScheme, uiKind, kiloCodeWrapperProperties)}>
+						onClick={() => {
+							vscode.postMessage({
+								type: "switchTab",
+								tab: "auth",
+								values: { returnTo: "settings", profileName: currentApiConfigName },
+							})
+						}}>
 						{t("kilocode:settings.provider.login")}
-					</VSCodeButtonLink>
+					</Button>
 				))}
 
 			<VSCodeTextField

+ 1 - 10
webview-ui/src/components/settings/ApiOptions.tsx

@@ -169,13 +169,7 @@ const ApiOptions = ({
 	currentApiConfigName, // kilocode_change
 }: ApiOptionsProps) => {
 	const { t } = useAppTranslation()
-	const {
-		organizationAllowList,
-		uiKind, // kilocode_change
-		kiloCodeWrapperProperties, // kilocode_change
-		kilocodeDefaultModel,
-		cloudIsAuthenticated,
-	} = useExtensionState()
+	const { organizationAllowList, kilocodeDefaultModel, cloudIsAuthenticated } = useExtensionState()
 
 	const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
 		const headers = apiConfiguration?.openAiHeaders || {}
@@ -575,9 +569,6 @@ const ApiOptions = ({
 					currentApiConfigName={currentApiConfigName}
 					routerModels={routerModels}
 					organizationAllowList={organizationAllowList}
-					uriScheme={uriScheme}
-					uiKind={uiKind}
-					kiloCodeWrapperProperties={kiloCodeWrapperProperties}
 					kilocodeDefaultModel={kilocodeDefaultModel}
 				/>
 			)}

+ 19 - 0
webview-ui/src/i18n/locales/en/kilocode.json

@@ -286,5 +286,24 @@
 	},
 	"modes": {
 		"shareModesNewBanner": "New: Share modes by creating an organization"
+	},
+	"deviceAuth": {
+		"title": "Sign in to Kilo Code",
+		"step1": "Open the following URL on your browser",
+		"step2": "Verify the code and authorize this device on your browser",
+		"scanQr": "Or scan this QR code with your phone",
+		"openBrowser": "Open Browser",
+		"copyUrl": "Copy URL",
+		"waiting": "Waiting for authorization...",
+		"timeRemaining": "Code expires in {{time}}",
+		"success": "Successfully signed in!",
+		"authenticatedAs": "Authenticated as {{email}}",
+		"error": "Authentication Error",
+		"unknownError": "An error occurred. Please try again.",
+		"cancel": "Cancel",
+		"retry": "Try Again",
+		"tryAgain": "Try Again",
+		"cancelled": "Authentication Cancelled",
+		"initiating": "Starting authentication..."
 	}
 }

+ 68 - 0
webview-ui/src/utils/kilocode/qrcode.ts

@@ -0,0 +1,68 @@
+import QRCode from "qrcode"
+
+/**
+ * Generate a QR code as a data URL
+ * @param text The text to encode in the QR code
+ * @param options QR code generation options
+ * @returns Promise resolving to a data URL string
+ */
+export async function generateQRCode(
+	text: string,
+	options?: {
+		width?: number
+		margin?: number
+		color?: {
+			dark?: string
+			light?: string
+		}
+	},
+): Promise<string> {
+	try {
+		const dataUrl = await QRCode.toDataURL(text, {
+			width: options?.width ?? 200,
+			margin: options?.margin ?? 2,
+			color: {
+				dark: options?.color?.dark ?? "#000000",
+				light: options?.color?.light ?? "#FFFFFF",
+			},
+		})
+		return dataUrl
+	} catch (error) {
+		console.error("Failed to generate QR code:", error)
+		throw new Error(`Failed to generate QR code: ${error instanceof Error ? error.message : String(error)}`)
+	}
+}
+
+/**
+ * Generate a QR code as an SVG string
+ * @param text The text to encode in the QR code
+ * @param options QR code generation options
+ * @returns Promise resolving to an SVG string
+ */
+export async function generateQRCodeSVG(
+	text: string,
+	options?: {
+		width?: number
+		margin?: number
+		color?: {
+			dark?: string
+			light?: string
+		}
+	},
+): Promise<string> {
+	try {
+		const svg = await QRCode.toString(text, {
+			type: "svg",
+			width: options?.width ?? 200,
+			margin: options?.margin ?? 2,
+			color: {
+				dark: options?.color?.dark ?? "#000000",
+				light: options?.color?.light ?? "#FFFFFF",
+			},
+		})
+		return svg
+	} catch (error) {
+		console.error("Failed to generate QR code SVG:", error)
+		throw new Error(`Failed to generate QR code SVG: ${error instanceof Error ? error.message : String(error)}`)
+	}
+}