diff --git a/.changeset/gentle-plants-smile.md b/.changeset/gentle-plants-smile.md new file mode 100644 index 00000000000..5e7c4371e01 --- /dev/null +++ b/.changeset/gentle-plants-smile.md @@ -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. diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9d667ae471f..ca2aac9b6f2 100644 --- a/packages/types/src/index.ts +++ b/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 diff --git a/packages/types/src/kilocode/device-auth.ts b/packages/types/src/kilocode/device-auth.ts new file mode 100644 index 00000000000..bf47c84be71 --- /dev/null +++ b/packages/types/src/kilocode/device-auth.ts @@ -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 + +/** + * 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 + +/** + * 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 +} diff --git a/src/core/kilocode/webview/deviceAuthHandler.ts b/src/core/kilocode/webview/deviceAuthHandler.ts new file mode 100644 index 00000000000..c90019bc70b --- /dev/null +++ b/src/core/kilocode/webview/deviceAuthHandler.ts @@ -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 + 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 { + 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 + } + } +} diff --git a/src/core/kilocode/webview/webviewMessageHandlerUtils.ts b/src/core/kilocode/webview/webviewMessageHandlerUtils.ts index 32019fd8c57..0b77326b392 100644 --- a/src/core/kilocode/webview/webviewMessageHandlerUtils.ts +++ b/src/core/kilocode/webview/webviewMessageHandlerUtils.ts @@ -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 => { + 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 + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a39d1eae321..2b08dcbc630 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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> // kilocode_change end @@ -157,6 +158,7 @@ export class ClineProvider private taskEventListeners: WeakMap 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 = new Map() @@ -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 @@ -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() @@ -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 diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c21846d4c2c..94c69877393 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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"]) @@ -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}`) // diff --git a/src/services/kilocode/DeviceAuthService.ts b/src/services/kilocode/DeviceAuthService.ts new file mode 100644 index 00000000000..adf4cb0e476 --- /dev/null +++ b/src/services/kilocode/DeviceAuthService.ts @@ -0,0 +1,195 @@ +import EventEmitter from "events" +import { getApiUrl, DeviceAuthInitiateResponseSchema, DeviceAuthPollResponseSchema } 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 { + 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 { + 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() + + // Validate the response against the schema + const validationResult = DeviceAuthInitiateResponseSchema.safeParse(data) + + if (!validationResult.success) { + console.error("[DeviceAuthService] Invalid initiate response format", { + errors: validationResult.error.errors, + }) + // Continue with unvalidated data for graceful degradation + } + + const validatedData = validationResult.success + ? validationResult.data + : (data as DeviceAuthInitiateResponse) + + this.code = validatedData.code + this.expiresIn = validatedData.expiresIn + this.startTime = Date.now() + this.aborted = false + + this.emit("started", validatedData) + + // 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 { + 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() + + // Validate the response against the schema + const validationResult = DeviceAuthPollResponseSchema.safeParse(data) + + if (!validationResult.success) { + console.error("[DeviceAuthService] Invalid poll response format", { + errors: validationResult.error.errors, + }) + // Continue with unvalidated data for graceful degradation + } + + const validatedData = validationResult.success ? validationResult.data : (data as DeviceAuthPollResponse) + + if (validatedData.status === "approved" && validatedData.token && validatedData.userEmail) { + this.emit("success", validatedData.token, validatedData.userEmail) + } else if (validatedData.status === "denied") { + this.emit("denied") + } else if (validatedData.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() + } +} diff --git a/src/services/kilocode/__tests__/DeviceAuthService.test.ts b/src/services/kilocode/__tests__/DeviceAuthService.test.ts new file mode 100644 index 00000000000..15818ac2ff9 --- /dev/null +++ b/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: "test@example.com", + } + + // 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", "test@example.com") + }) + + 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() + }) + }) +}) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 9c5a4a5ba51..b1bc72a57d2 100644 --- a/src/shared/ExtensionMessage.ts +++ b/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< diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index a74b59e3db5..98b19d2496e 100644 --- a/src/shared/WebviewMessage.ts +++ b/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 diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 936f32455f0..14f9ca13a8d 100644 --- a/webview-ui/src/App.tsx +++ b/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("chat") + const [authReturnTo, setAuthReturnTo] = useState<"chat" | "settings">("chat") + const [authProfileName, setAuthProfileName] = useState(undefined) + const [settingsEditingProfile, setSettingsEditingProfile] = useState(undefined) const [humanRelayDialogState, setHumanRelayDialogState] = useState({ isOpen: false, @@ -151,7 +155,11 @@ const App = () => { setCurrentSection(undefined) setCurrentMarketplaceTab(undefined) - if (settingsRef.current?.checkUnsaveChanges) { + // kilocode_change: start - 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" && switchTab("chat")} />} {tab === "mcp" && switchTab("chat")} />} {tab === "history" && switchTab("chat")} />} + {/* kilocode_change: auth redirect / editingProfile */} {tab === "settings" && ( - switchTab("chat")} targetSection={currentSection} /> // kilocode_change + switchTab("chat")} + targetSection={currentSection} + editingProfile={settingsEditingProfile} + /> )} - {/* kilocode_change: add profileview */} + {/* kilocode_change: add profileview and authview */} {tab === "profile" && switchTab("chat")} />} + {tab === "auth" && } {tab === "marketplace" && ( = ({ returnTo = "chat", profileName }) => { + const [deviceAuthStatus, setDeviceAuthStatus] = useState("idle") + const [deviceAuthCode, setDeviceAuthCode] = useState() + const [deviceAuthVerificationUrl, setDeviceAuthVerificationUrl] = useState() + const [deviceAuthExpiresIn, setDeviceAuthExpiresIn] = useState() + const [deviceAuthTimeRemaining, setDeviceAuthTimeRemaining] = useState() + const [deviceAuthError, setDeviceAuthError] = useState() + const [deviceAuthUserEmail, setDeviceAuthUserEmail] = useState() + + // 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 ( + + + + + + ) +} + +export default AuthView diff --git a/webview-ui/src/components/kilocode/common/DeviceAuthCard.tsx b/webview-ui/src/components/kilocode/common/DeviceAuthCard.tsx new file mode 100644 index 00000000000..4d2d502ac92 --- /dev/null +++ b/webview-ui/src/components/kilocode/common/DeviceAuthCard.tsx @@ -0,0 +1,281 @@ +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 +} + +// Inner component for initiating state +const InitiatingState: React.FC = () => { + const { t } = useAppTranslation() + return ( +
+ +
+ + {t("kilocode:deviceAuth.initiating")} +
+
+ ) +} + +// Inner component for success state +interface SuccessStateProps { + userEmail?: string +} + +const SuccessState: React.FC = ({ userEmail }) => { + const { t } = useAppTranslation() + return ( +
+ +

{t("kilocode:deviceAuth.success")}

+ {userEmail && ( +

+ {t("kilocode:deviceAuth.authenticatedAs", { email: userEmail })} +

+ )} +
+ ) +} + +// Inner component for error state +interface ErrorStateProps { + error?: string + onRetry: () => void +} + +const ErrorState: React.FC = ({ error, onRetry }) => { + const { t } = useAppTranslation() + return ( +
+ +

{t("kilocode:deviceAuth.error")}

+

+ {error || t("kilocode:deviceAuth.unknownError")} +

+ {t("kilocode:deviceAuth.retry")} +
+ ) +} + +// Inner component for cancelled state +interface CancelledStateProps { + onRetry: () => void +} + +const CancelledState: React.FC = ({ onRetry }) => { + const { t } = useAppTranslation() + return ( +
+ +

{t("kilocode:deviceAuth.cancelled")}

+ {t("kilocode:deviceAuth.tryAgain")} +
+ ) +} + +// Inner component for pending state +interface PendingStateProps { + code: string + verificationUrl: string + qrCodeDataUrl: string + timeRemaining?: number + formatTime: (seconds?: number) => string + onOpenBrowser: () => void + onCancel: () => void +} + +const PendingState: React.FC = ({ + code, + verificationUrl, + qrCodeDataUrl, + timeRemaining, + formatTime, + onOpenBrowser, + onCancel, +}) => { + const { t } = useAppTranslation() + const handleCopyUrl = () => { + navigator.clipboard.writeText(verificationUrl) + } + + return ( +
+

+ {t("kilocode:deviceAuth.title")} +

+ + {/* Step 1: URL Section */} +
+

+ {t("kilocode:deviceAuth.step1")} +

+ + {/* URL Box with Copy and Open Browser */} +
+
+ + {verificationUrl} + + +
+ {t("kilocode:deviceAuth.openBrowser")} +
+ + {/* QR Code Section */} + {qrCodeDataUrl && ( +
+

{t("kilocode:deviceAuth.scanQr")}

+ QR Code +
+ )} +
+ + {/* Step 2: Verification Section */} +
+

+ {t("kilocode:deviceAuth.step2")} +

+ + {/* Verification Code */} +
+
+ + {code} + +
+
+ + {/* Time Remaining */} +
+ + + {t("kilocode:deviceAuth.timeRemaining", { time: formatTime(timeRemaining) })} + +
+ + {/* Status */} +
+ + + {t("kilocode:deviceAuth.waiting")} + +
+
+ + {/* Cancel Button */} +
+ {t("kilocode:deviceAuth.cancel")} +
+
+ ) +} + +const DeviceAuthCard: React.FC = ({ + code, + verificationUrl, + timeRemaining, + status, + error, + userEmail, + onCancel, + onRetry, +}) => { + const [qrCodeDataUrl, setQrCodeDataUrl] = useState("") + + // 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 + } + + if (status === "success") { + return + } + + if (status === "error") { + return + } + + if (status === "cancelled") { + return + } + + // Pending state - show code and QR + if (status === "pending" && code && verificationUrl) { + return ( + + ) + } + + // Idle state - shouldn't normally be shown + return null +} + +export default DeviceAuthCard diff --git a/webview-ui/src/components/kilocode/common/KiloCodeAuth.tsx b/webview-ui/src/components/kilocode/common/KiloCodeAuth.tsx index 09868473b66..c52e1012c84 100644 --- a/webview-ui/src/components/kilocode/common/KiloCodeAuth.tsx +++ b/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 = ({ onManualConfigClick, className = "" }) => { - const { uriScheme, uiKind, kiloCodeWrapperProperties } = useExtensionState() +type DeviceAuthStatus = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled" +const KiloCodeAuth: React.FC = ({ onManualConfigClick, onLoginClick, className = "" }) => { const { t } = useAppTranslation() + const [deviceAuthStatus, setDeviceAuthStatus] = useState("idle") + const [deviceAuthCode, setDeviceAuthCode] = useState() + const [deviceAuthVerificationUrl, setDeviceAuthVerificationUrl] = useState() + const [deviceAuthExpiresIn, setDeviceAuthExpiresIn] = useState() + const [deviceAuthTimeRemaining, setDeviceAuthTimeRemaining] = useState() + const [deviceAuthError, setDeviceAuthError] = useState() + const [deviceAuthUserEmail, setDeviceAuthUserEmail] = useState() + // 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 ( +
+ +
+ ) + } + + // Default welcome screen return (
@@ -26,15 +136,7 @@ const KiloCodeAuth: React.FC = ({ onManualConfigClick, classN

{t("kilocode:welcome.introText3")}

- { - if (uiKind === "Web" && onManualConfigClick) { - onManualConfigClick() - } - }}> - {t("kilocode:welcome.ctaButton")} - + {t("kilocode:welcome.ctaButton")} {!!onManualConfigClick && ( onManualConfigClick && onManualConfigClick()}> diff --git a/webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx b/webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx index d38e1619b3b..e8f444df83a 100644 --- a/webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx +++ b/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 = ({
) : ( - + onClick={() => { + vscode.postMessage({ + type: "switchTab", + tab: "auth", + values: { returnTo: "settings", profileName: currentApiConfigName }, + }) + }}> {t("kilocode:settings.provider.login")} - + ))} { 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} /> )} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index dd5deb2493b..e58e41522cc 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -120,9 +120,13 @@ type SectionName = (typeof sectionNames)[number] // kilocode_change type SettingsViewProps = { onDone: () => void targetSection?: string + editingProfile?: string // kilocode_change - profile to edit } -const SettingsView = forwardRef(({ onDone, targetSection }, ref) => { +// kilocode_change start - editingProfile +const SettingsView = forwardRef((props, ref) => { + const { onDone, targetSection, editingProfile } = props + // kilocode_change end - editingProfile const { t } = useAppTranslation() const extensionState = useExtensionState() @@ -270,8 +274,26 @@ const SettingsView = forwardRef(({ onDone, t setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) prevApiConfigName.current = currentApiConfigName setChangeDetected(false) - setEditingApiConfigName(currentApiConfigName || "default") // kilocode_change: Sync editing profile when active profile changes - }, [currentApiConfigName, extensionState]) + // kilocode_change start - Don't reset editingApiConfigName if we have an editingProfile prop (from auth return) + if (!editingProfile) { + setEditingApiConfigName(currentApiConfigName || "default") + } + // kilocode_change end + }, [currentApiConfigName, extensionState, editingProfile]) // kilocode_change + + // kilocode_change start: Set editing profile when prop changes (from auth return) + useEffect(() => { + if (editingProfile) { + console.log("[SettingsView] Setting editing profile from prop:", editingProfile) + setEditingApiConfigName(editingProfile) + isLoadingProfileForEditing.current = true + vscode.postMessage({ + type: "getProfileConfigurationForEditing", + text: editingProfile, + }) + } + }, [editingProfile]) + // kilocode_change end // kilocode_change start const isLoadingProfileForEditing = useRef(false) @@ -741,6 +763,32 @@ const SettingsView = forwardRef(({ onDone, t } }, [targetSection]) // kilocode_change + // kilocode_change start - Listen for messages to restore editing profile after auth + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if ( + message.type === "action" && + message.action === "settingsButtonClicked" && + message.values?.editingProfile + ) { + const profileToEdit = message.values.editingProfile as string + console.log("[SettingsView] Restoring editing profile:", profileToEdit) + setEditingApiConfigName(profileToEdit) + // Request the profile's configuration for editing + isLoadingProfileForEditing.current = true + vscode.postMessage({ + type: "getProfileConfigurationForEditing", + text: profileToEdit, + }) + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, []) + // kilocode_change end + // Function to scroll the active tab into view for vertical layout const scrollToActiveTab = useCallback(() => { const activeTabElement = tabRefs.current[activeTab] @@ -951,14 +999,16 @@ const SettingsView = forwardRef(({ onDone, t /> {/* kilocode_change end changes to allow for editting a non-active profile */} + {/* kilocode_change start - pass editing profile name */} + {/* kilocode_change end - pass editing profile name */}
)} diff --git a/webview-ui/src/i18n/locales/ar/kilocode.json b/webview-ui/src/i18n/locales/ar/kilocode.json index aedbda964ac..54f81ecd645 100644 --- a/webview-ui/src/i18n/locales/ar/kilocode.json +++ b/webview-ui/src/i18n/locales/ar/kilocode.json @@ -287,5 +287,24 @@ }, "modes": { "shareModesNewBanner": "جديد: مشاركة الأوضاع عن طريق إنشاء منظمة" + }, + "deviceAuth": { + "title": "تسجيل الدخول إلى Kilo Code", + "step1": "افتح الرابط التالي في متصفحك", + "step2": "تحقق من الرمز وصرّح لهذا الجهاز في متصفحك", + "scanQr": "أو امسح رمز QR هذا بهاتفك", + "openBrowser": "فتح المتصفح", + "copyUrl": "نسخ الرابط", + "waiting": "في انتظار التصريح...", + "timeRemaining": "ينتهي الرمز خلال {{time}}", + "success": "تم تسجيل الدخول بنجاح!", + "authenticatedAs": "تم المصادقة كـ {{email}}", + "error": "خطأ في المصادقة", + "unknownError": "حدث خطأ. يرجى المحاولة مرة أخرى.", + "cancel": "إلغاء", + "retry": "حاول مرة أخرى", + "tryAgain": "حاول مرة أخرى", + "cancelled": "تم إلغاء المصادقة", + "initiating": "جاري بدء المصادقة..." } } diff --git a/webview-ui/src/i18n/locales/ca/kilocode.json b/webview-ui/src/i18n/locales/ca/kilocode.json index 6230fc8fd91..4760a58929c 100644 --- a/webview-ui/src/i18n/locales/ca/kilocode.json +++ b/webview-ui/src/i18n/locales/ca/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Nou: Comparteix modes creant una organització" + }, + "deviceAuth": { + "title": "Inicia sessió a Kilo Code", + "step1": "Obre la següent URL al teu navegador", + "step2": "Verifica el codi i autoritza aquest dispositiu al teu navegador", + "scanQr": "O escaneja aquest codi QR amb el teu telèfon", + "openBrowser": "Obre el navegador", + "copyUrl": "Copia l'URL", + "waiting": "Esperant autorització...", + "timeRemaining": "El codi caduca en {{time}}", + "success": "Sessió iniciada correctament!", + "authenticatedAs": "Autenticat com a {{email}}", + "error": "Error d'autenticació", + "unknownError": "S'ha produït un error. Torna-ho a provar.", + "cancel": "Cancel·la", + "retry": "Torna-ho a provar", + "tryAgain": "Torna-ho a provar", + "cancelled": "Autenticació cancel·lada", + "initiating": "Iniciant autenticació..." } } diff --git a/webview-ui/src/i18n/locales/cs/kilocode.json b/webview-ui/src/i18n/locales/cs/kilocode.json index ef39d082eaf..54372de391d 100644 --- a/webview-ui/src/i18n/locales/cs/kilocode.json +++ b/webview-ui/src/i18n/locales/cs/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Novinka: Sdílejte režimy vytvořením organizace" + }, + "deviceAuth": { + "title": "Přihlásit se do Kilo Code", + "step1": "Otevři následující URL ve svém prohlížeči", + "step2": "Ověř kód a autorizuj toto zařízení ve svém prohlížeči", + "scanQr": "Nebo naskenuj tento QR kód svým telefonem", + "openBrowser": "Otevřít prohlížeč", + "copyUrl": "Kopírovat URL", + "waiting": "Čekání na autorizaci...", + "timeRemaining": "Kód vyprší za {{time}}", + "success": "Úspěšně přihlášen!", + "authenticatedAs": "Ověřen jako {{email}}", + "error": "Chyba ověření", + "unknownError": "Došlo k chybě. Zkus to prosím znovu.", + "cancel": "Zrušit", + "retry": "Zkusit znovu", + "tryAgain": "Zkusit znovu", + "cancelled": "Ověření zrušeno", + "initiating": "Spouštění ověření..." } } diff --git a/webview-ui/src/i18n/locales/de/kilocode.json b/webview-ui/src/i18n/locales/de/kilocode.json index 224bd86d5cd..1c3f3b26fbe 100644 --- a/webview-ui/src/i18n/locales/de/kilocode.json +++ b/webview-ui/src/i18n/locales/de/kilocode.json @@ -287,5 +287,24 @@ }, "modes": { "shareModesNewBanner": "Neu: Modi teilen durch Erstellen einer Organisation" + }, + "deviceAuth": { + "title": "Bei Kilo Code anmelden", + "step1": "Öffne die folgende URL in deinem Browser", + "step2": "Überprüfe den Code und autorisiere dieses Gerät in deinem Browser", + "scanQr": "Oder scanne diesen QR-Code mit deinem Handy", + "openBrowser": "Browser öffnen", + "copyUrl": "URL kopieren", + "waiting": "Warte auf Autorisierung...", + "timeRemaining": "Code läuft ab in {{time}}", + "success": "Erfolgreich angemeldet!", + "authenticatedAs": "Authentifiziert als {{email}}", + "error": "Authentifizierungsfehler", + "unknownError": "Ein Fehler ist aufgetreten. Bitte versuche es erneut.", + "cancel": "Abbrechen", + "retry": "Erneut versuchen", + "tryAgain": "Erneut versuchen", + "cancelled": "Authentifizierung abgebrochen", + "initiating": "Authentifizierung wird gestartet..." } } diff --git a/webview-ui/src/i18n/locales/en/kilocode.json b/webview-ui/src/i18n/locales/en/kilocode.json index e840fb66a90..690150688b6 100644 --- a/webview-ui/src/i18n/locales/en/kilocode.json +++ b/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..." } } diff --git a/webview-ui/src/i18n/locales/es/kilocode.json b/webview-ui/src/i18n/locales/es/kilocode.json index 102763c7d26..721f7c79cc1 100644 --- a/webview-ui/src/i18n/locales/es/kilocode.json +++ b/webview-ui/src/i18n/locales/es/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Nuevo: Comparte modos creando una organización" + }, + "deviceAuth": { + "title": "Iniciar sesión en Kilo Code", + "step1": "Abre la siguiente URL en tu navegador", + "step2": "Verifica el código y autoriza este dispositivo en tu navegador", + "scanQr": "O escanea este código QR con tu teléfono", + "openBrowser": "Abrir navegador", + "copyUrl": "Copiar URL", + "waiting": "Esperando autorización...", + "timeRemaining": "El código expira en {{time}}", + "success": "¡Sesión iniciada correctamente!", + "authenticatedAs": "Autenticado como {{email}}", + "error": "Error de autenticación", + "unknownError": "Ocurrió un error. Por favor, inténtalo de nuevo.", + "cancel": "Cancelar", + "retry": "Intentar de nuevo", + "tryAgain": "Intentar de nuevo", + "cancelled": "Autenticación cancelada", + "initiating": "Iniciando autenticación..." } } diff --git a/webview-ui/src/i18n/locales/fr/kilocode.json b/webview-ui/src/i18n/locales/fr/kilocode.json index dc4a97272e8..939e6aa0d9e 100644 --- a/webview-ui/src/i18n/locales/fr/kilocode.json +++ b/webview-ui/src/i18n/locales/fr/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Nouveau : Partagez des modes en créant une organisation" + }, + "deviceAuth": { + "title": "Se connecter à Kilo Code", + "step1": "Ouvre l'URL suivante dans ton navigateur", + "step2": "Vérifie le code et autorise cet appareil dans ton navigateur", + "scanQr": "Ou scanne ce code QR avec ton téléphone", + "openBrowser": "Ouvrir le navigateur", + "copyUrl": "Copier l'URL", + "waiting": "En attente d'autorisation...", + "timeRemaining": "Le code expire dans {{time}}", + "success": "Connexion réussie !", + "authenticatedAs": "Authentifié en tant que {{email}}", + "error": "Erreur d'authentification", + "unknownError": "Une erreur s'est produite. Réessaye.", + "cancel": "Annuler", + "retry": "Réessayer", + "tryAgain": "Réessayer", + "cancelled": "Authentification annulée", + "initiating": "Démarrage de l'authentification..." } } diff --git a/webview-ui/src/i18n/locales/hi/kilocode.json b/webview-ui/src/i18n/locales/hi/kilocode.json index f7e1bf27624..a0d359aa7f3 100644 --- a/webview-ui/src/i18n/locales/hi/kilocode.json +++ b/webview-ui/src/i18n/locales/hi/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "नया: एक संगठन बनाकर मोड साझा करें" + }, + "deviceAuth": { + "title": "Kilo Code में साइन इन करें", + "step1": "अपने ब्राउज़र में निम्नलिखित URL खोलें", + "step2": "कोड सत्यापित करें और अपने ब्राउज़र में इस डिवाइस को अधिकृत करें", + "scanQr": "या अपने फोन से इस QR कोड को स्कैन करें", + "openBrowser": "ब्राउज़र खोलें", + "copyUrl": "URL कॉपी करें", + "waiting": "प्राधिकरण की प्रतीक्षा में...", + "timeRemaining": "कोड {{time}} में समाप्त हो जाएगा", + "success": "सफलतापूर्वक साइन इन हो गया!", + "authenticatedAs": "{{email}} के रूप में प्रमाणित", + "error": "प्रमाणीकरण त्रुटि", + "unknownError": "एक त्रुटि हुई। कृपया पुनः प्रयास करें।", + "cancel": "रद्द करें", + "retry": "पुनः प्रयास करें", + "tryAgain": "पुनः प्रयास करें", + "cancelled": "प्रमाणीकरण रद्द किया गया", + "initiating": "प्रमाणीकरण शुरू हो रहा है..." } } diff --git a/webview-ui/src/i18n/locales/id/kilocode.json b/webview-ui/src/i18n/locales/id/kilocode.json index 9034c7cc4ec..9e3aea2895c 100644 --- a/webview-ui/src/i18n/locales/id/kilocode.json +++ b/webview-ui/src/i18n/locales/id/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Baru: Bagikan mode dengan membuat organisasi" + }, + "deviceAuth": { + "title": "Masuk ke Kilo Code", + "step1": "Buka URL berikut di browser kamu", + "step2": "Verifikasi kode dan otorisasi perangkat ini di browser kamu", + "scanQr": "Atau pindai kode QR ini dengan ponsel kamu", + "openBrowser": "Buka Browser", + "copyUrl": "Salin URL", + "waiting": "Menunggu otorisasi...", + "timeRemaining": "Kode kedaluwarsa dalam {{time}}", + "success": "Berhasil masuk!", + "authenticatedAs": "Diautentikasi sebagai {{email}}", + "error": "Kesalahan Autentikasi", + "unknownError": "Terjadi kesalahan. Silakan coba lagi.", + "cancel": "Batal", + "retry": "Coba Lagi", + "tryAgain": "Coba Lagi", + "cancelled": "Autentikasi Dibatalkan", + "initiating": "Memulai autentikasi..." } } diff --git a/webview-ui/src/i18n/locales/it/kilocode.json b/webview-ui/src/i18n/locales/it/kilocode.json index c1bde89280a..7c9a5e4497c 100644 --- a/webview-ui/src/i18n/locales/it/kilocode.json +++ b/webview-ui/src/i18n/locales/it/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Novità: Condividi le modalità creando un'organizzazione" + }, + "deviceAuth": { + "title": "Accedi a Kilo Code", + "step1": "Apri il seguente URL nel tuo browser", + "step2": "Verifica il codice e autorizza questo dispositivo nel tuo browser", + "scanQr": "Oppure scansiona questo codice QR con il tuo telefono", + "openBrowser": "Apri Browser", + "copyUrl": "Copia URL", + "waiting": "In attesa di autorizzazione...", + "timeRemaining": "Il codice scade tra {{time}}", + "success": "Accesso effettuato con successo!", + "authenticatedAs": "Autenticato come {{email}}", + "error": "Errore di autenticazione", + "unknownError": "Si è verificato un errore. Riprova.", + "cancel": "Annulla", + "retry": "Riprova", + "tryAgain": "Riprova", + "cancelled": "Autenticazione annullata", + "initiating": "Avvio autenticazione..." } } diff --git a/webview-ui/src/i18n/locales/ja/kilocode.json b/webview-ui/src/i18n/locales/ja/kilocode.json index 4c639729a3d..013534a41bd 100644 --- a/webview-ui/src/i18n/locales/ja/kilocode.json +++ b/webview-ui/src/i18n/locales/ja/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "新機能: 組織を作成してモードを共有" + }, + "deviceAuth": { + "title": "Kilo Code にサインイン", + "step1": "ブラウザで次のURLを開いてください", + "step2": "コードを確認し、ブラウザでこのデバイスを承認してください", + "scanQr": "またはスマートフォンでこのQRコードをスキャンしてください", + "openBrowser": "ブラウザを開く", + "copyUrl": "URLをコピー", + "waiting": "承認を待っています...", + "timeRemaining": "コードは {{time}} で期限切れになります", + "success": "サインインに成功しました!", + "authenticatedAs": "{{email}} として認証されました", + "error": "認証エラー", + "unknownError": "エラーが発生しました。もう一度お試しください。", + "cancel": "キャンセル", + "retry": "再試行", + "tryAgain": "再試行", + "cancelled": "認証がキャンセルされました", + "initiating": "認証を開始しています..." } } diff --git a/webview-ui/src/i18n/locales/ko/kilocode.json b/webview-ui/src/i18n/locales/ko/kilocode.json index 5b5ad72fb01..0dea850cd5d 100644 --- a/webview-ui/src/i18n/locales/ko/kilocode.json +++ b/webview-ui/src/i18n/locales/ko/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "신규: 조직을 생성하여 모드 공유하기" + }, + "deviceAuth": { + "title": "Kilo Code에 로그인", + "step1": "브라우저에서 다음 URL을 여세요", + "step2": "코드를 확인하고 브라우저에서 이 기기를 승인하세요", + "scanQr": "또는 휴대폰으로 이 QR 코드를 스캔하세요", + "openBrowser": "브라우저 열기", + "copyUrl": "URL 복사", + "waiting": "승인 대기 중...", + "timeRemaining": "코드가 {{time}} 후에 만료됩니다", + "success": "로그인에 성공했습니다!", + "authenticatedAs": "{{email}}(으)로 인증됨", + "error": "인증 오류", + "unknownError": "오류가 발생했습니다. 다시 시도해 주세요.", + "cancel": "취소", + "retry": "다시 시도", + "tryAgain": "다시 시도", + "cancelled": "인증이 취소되었습니다", + "initiating": "인증을 시작하는 중..." } } diff --git a/webview-ui/src/i18n/locales/nl/kilocode.json b/webview-ui/src/i18n/locales/nl/kilocode.json index f8d9bc161bf..5ac6ba13358 100644 --- a/webview-ui/src/i18n/locales/nl/kilocode.json +++ b/webview-ui/src/i18n/locales/nl/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Nieuw: Deel modi door een organisatie aan te maken" + }, + "deviceAuth": { + "title": "Inloggen bij Kilo Code", + "step1": "Open de volgende URL in je browser", + "step2": "Verifieer de code en autoriseer dit apparaat in je browser", + "scanQr": "Of scan deze QR-code met je telefoon", + "openBrowser": "Browser openen", + "copyUrl": "URL kopiëren", + "waiting": "Wachten op autorisatie...", + "timeRemaining": "Code verloopt over {{time}}", + "success": "Succesvol ingelogd!", + "authenticatedAs": "Geauthenticeerd als {{email}}", + "error": "Authenticatiefout", + "unknownError": "Er is een fout opgetreden. Probeer het opnieuw.", + "cancel": "Annuleren", + "retry": "Opnieuw proberen", + "tryAgain": "Opnieuw proberen", + "cancelled": "Authenticatie geannuleerd", + "initiating": "Authenticatie starten..." } } diff --git a/webview-ui/src/i18n/locales/pl/kilocode.json b/webview-ui/src/i18n/locales/pl/kilocode.json index 6b21b418cfd..23119580fd3 100644 --- a/webview-ui/src/i18n/locales/pl/kilocode.json +++ b/webview-ui/src/i18n/locales/pl/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Nowość: Udostępniaj tryby tworząc organizację" + }, + "deviceAuth": { + "title": "Zaloguj się do Kilo Code", + "step1": "Otwórz następujący adres URL w przeglądarce", + "step2": "Zweryfikuj kod i autoryzuj to urządzenie w przeglądarce", + "scanQr": "Lub zeskanuj ten kod QR telefonem", + "openBrowser": "Otwórz przeglądarkę", + "copyUrl": "Kopiuj URL", + "waiting": "Oczekiwanie na autoryzację...", + "timeRemaining": "Kod wygasa za {{time}}", + "success": "Pomyślnie zalogowano!", + "authenticatedAs": "Uwierzytelniono jako {{email}}", + "error": "Błąd uwierzytelniania", + "unknownError": "Wystąpił błąd. Spróbuj ponownie.", + "cancel": "Anuluj", + "retry": "Spróbuj ponownie", + "tryAgain": "Spróbuj ponownie", + "cancelled": "Uwierzytelnianie anulowane", + "initiating": "Rozpoczynanie uwierzytelniania..." } } diff --git a/webview-ui/src/i18n/locales/pt-BR/kilocode.json b/webview-ui/src/i18n/locales/pt-BR/kilocode.json index 92f9e870856..f0ce5d0b78a 100644 --- a/webview-ui/src/i18n/locales/pt-BR/kilocode.json +++ b/webview-ui/src/i18n/locales/pt-BR/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Novo: Compartilhe modos criando uma organização" + }, + "deviceAuth": { + "title": "Entrar no Kilo Code", + "step1": "Abra a seguinte URL no seu navegador", + "step2": "Verifique o código e autorize este dispositivo no seu navegador", + "scanQr": "Ou escaneie este código QR com seu telefone", + "openBrowser": "Abrir navegador", + "copyUrl": "Copiar URL", + "waiting": "Aguardando autorização...", + "timeRemaining": "O código expira em {{time}}", + "success": "Login realizado com sucesso!", + "authenticatedAs": "Autenticado como {{email}}", + "error": "Erro de autenticação", + "unknownError": "Ocorreu um erro. Por favor, tente novamente.", + "cancel": "Cancelar", + "retry": "Tentar novamente", + "tryAgain": "Tentar novamente", + "cancelled": "Autenticação cancelada", + "initiating": "Iniciando autenticação..." } } diff --git a/webview-ui/src/i18n/locales/ru/kilocode.json b/webview-ui/src/i18n/locales/ru/kilocode.json index ff7a8aeb732..a38fd74cbb2 100644 --- a/webview-ui/src/i18n/locales/ru/kilocode.json +++ b/webview-ui/src/i18n/locales/ru/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Новое: Делитесь режимами путем создания организации" + }, + "deviceAuth": { + "title": "Войти в Kilo Code", + "step1": "Открой следующий URL в своем браузере", + "step2": "Проверь код и авторизуй это устройство в своем браузере", + "scanQr": "Или отсканируй этот QR-код своим телефоном", + "openBrowser": "Открыть браузер", + "copyUrl": "Скопировать URL", + "waiting": "Ожидание авторизации...", + "timeRemaining": "Код истекает через {{time}}", + "success": "Успешный вход!", + "authenticatedAs": "Аутентифицирован как {{email}}", + "error": "Ошибка аутентификации", + "unknownError": "Произошла ошибка. Попробуй снова.", + "cancel": "Отмена", + "retry": "Попробовать снова", + "tryAgain": "Попробовать снова", + "cancelled": "Аутентификация отменена", + "initiating": "Запуск аутентификации..." } } diff --git a/webview-ui/src/i18n/locales/th/kilocode.json b/webview-ui/src/i18n/locales/th/kilocode.json index 6571dffc2f2..31d6e9efc52 100644 --- a/webview-ui/src/i18n/locales/th/kilocode.json +++ b/webview-ui/src/i18n/locales/th/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "ใหม่: แชร์โหมดโดยการสร้างองค์กร" + }, + "deviceAuth": { + "title": "ลงชื่อเข้าใช้ Kilo Code", + "step1": "เปิด URL ต่อไปนี้ในเบราว์เซอร์ของคุณ", + "step2": "ตรวจสอบรหัสและอนุญาตอุปกรณ์นี้ในเบราว์เซอร์ของคุณ", + "scanQr": "หรือสแกนรหัส QR นี้ด้วยโทรศัพท์ของคุณ", + "openBrowser": "เปิดเบราว์เซอร์", + "copyUrl": "คัดลอก URL", + "waiting": "กำลังรอการอนุญาต...", + "timeRemaining": "รหัสจะหมดอายุใน {{time}}", + "success": "ลงชื่อเข้าใช้สำเร็จ!", + "authenticatedAs": "ยืนยันตัวตนเป็น {{email}}", + "error": "ข้อผิดพลาดในการยืนยันตัวตน", + "unknownError": "เกิดข้อผิดพลาด โปรดลองอีกครั้ง", + "cancel": "ยกเลิก", + "retry": "ลองอีกครั้ง", + "tryAgain": "ลองอีกครั้ง", + "cancelled": "ยกเลิกการยืนยันตัวตนแล้ว", + "initiating": "กำลังเริ่มการยืนยันตัวตน..." } } diff --git a/webview-ui/src/i18n/locales/tr/kilocode.json b/webview-ui/src/i18n/locales/tr/kilocode.json index add412ca3a6..3687d244b0c 100644 --- a/webview-ui/src/i18n/locales/tr/kilocode.json +++ b/webview-ui/src/i18n/locales/tr/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Yeni: Bir organizasyon oluşturarak modları paylaşın" + }, + "deviceAuth": { + "title": "Kilo Code'a giriş yap", + "step1": "Tarayıcında aşağıdaki URL'yi aç", + "step2": "Kodu doğrula ve bu cihazı tarayıcında yetkilendir", + "scanQr": "Veya bu QR kodunu telefonunla tara", + "openBrowser": "Tarayıcıyı Aç", + "copyUrl": "URL'yi Kopyala", + "waiting": "Yetkilendirme bekleniyor...", + "timeRemaining": "Kod {{time}} içinde sona erecek", + "success": "Başarıyla giriş yapıldı!", + "authenticatedAs": "{{email}} olarak kimlik doğrulandı", + "error": "Kimlik Doğrulama Hatası", + "unknownError": "Bir hata oluştu. Lütfen tekrar dene.", + "cancel": "İptal", + "retry": "Tekrar Dene", + "tryAgain": "Tekrar Dene", + "cancelled": "Kimlik Doğrulama İptal Edildi", + "initiating": "Kimlik doğrulama başlatılıyor..." } } diff --git a/webview-ui/src/i18n/locales/uk/kilocode.json b/webview-ui/src/i18n/locales/uk/kilocode.json index 98c3511d9f1..211de92853b 100644 --- a/webview-ui/src/i18n/locales/uk/kilocode.json +++ b/webview-ui/src/i18n/locales/uk/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Нове: Діліться режимами, створивши організацію" + }, + "deviceAuth": { + "title": "Увійти в Kilo Code", + "step1": "Відкрий наступний URL у своєму браузері", + "step2": "Перевір код і авторизуй цей пристрій у своєму браузері", + "scanQr": "Або відскануй цей QR-код своїм телефоном", + "openBrowser": "Відкрити браузер", + "copyUrl": "Скопіювати URL", + "waiting": "Очікування авторизації...", + "timeRemaining": "Код закінчується через {{time}}", + "success": "Успішний вхід!", + "authenticatedAs": "Автентифіковано як {{email}}", + "error": "Помилка автентифікації", + "unknownError": "Сталася помилка. Спробуй знову.", + "cancel": "Скасувати", + "retry": "Спробувати знову", + "tryAgain": "Спробувати знову", + "cancelled": "Автентифікацію скасовано", + "initiating": "Запуск автентифікації..." } } diff --git a/webview-ui/src/i18n/locales/vi/kilocode.json b/webview-ui/src/i18n/locales/vi/kilocode.json index 850f66f328d..928a3dc8538 100644 --- a/webview-ui/src/i18n/locales/vi/kilocode.json +++ b/webview-ui/src/i18n/locales/vi/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "Mới: Chia sẻ chế độ bằng cách tạo một tổ chức" + }, + "deviceAuth": { + "title": "Đăng nhập vào Kilo Code", + "step1": "Mở URL sau trên trình duyệt của bạn", + "step2": "Xác minh mã và ủy quyền thiết bị này trên trình duyệt của bạn", + "scanQr": "Hoặc quét mã QR này bằng điện thoại của bạn", + "openBrowser": "Mở trình duyệt", + "copyUrl": "Sao chép URL", + "waiting": "Đang chờ ủy quyền...", + "timeRemaining": "Mã hết hạn sau {{time}}", + "success": "Đăng nhập thành công!", + "authenticatedAs": "Đã xác thực với tư cách {{email}}", + "error": "Lỗi xác thực", + "unknownError": "Đã xảy ra lỗi. Vui lòng thử lại.", + "cancel": "Hủy", + "retry": "Thử lại", + "tryAgain": "Thử lại", + "cancelled": "Đã hủy xác thực", + "initiating": "Đang bắt đầu xác thực..." } } diff --git a/webview-ui/src/i18n/locales/zh-CN/kilocode.json b/webview-ui/src/i18n/locales/zh-CN/kilocode.json index 21ac26c6b40..fe55845bcf0 100644 --- a/webview-ui/src/i18n/locales/zh-CN/kilocode.json +++ b/webview-ui/src/i18n/locales/zh-CN/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "新功能:通过创建组织来共享模式" + }, + "deviceAuth": { + "title": "登录 Kilo Code", + "step1": "在浏览器中打开以下 URL", + "step2": "验证代码并在浏览器中授权此设备", + "scanQr": "或用手机扫描此二维码", + "openBrowser": "打开浏览器", + "copyUrl": "复制 URL", + "waiting": "等待授权...", + "timeRemaining": "代码将在 {{time}} 后过期", + "success": "登录成功!", + "authenticatedAs": "已验证为 {{email}}", + "error": "验证错误", + "unknownError": "发生错误。请重试。", + "cancel": "取消", + "retry": "重试", + "tryAgain": "重试", + "cancelled": "验证已取消", + "initiating": "正在启动验证..." } } diff --git a/webview-ui/src/i18n/locales/zh-TW/kilocode.json b/webview-ui/src/i18n/locales/zh-TW/kilocode.json index b6fc90fde1d..daa7077b1f4 100644 --- a/webview-ui/src/i18n/locales/zh-TW/kilocode.json +++ b/webview-ui/src/i18n/locales/zh-TW/kilocode.json @@ -288,5 +288,24 @@ }, "modes": { "shareModesNewBanner": "新功能:通过创建组织共享模式" + }, + "deviceAuth": { + "title": "登入 Kilo Code", + "step1": "在瀏覽器中開啟以下 URL", + "step2": "驗證代碼並在瀏覽器中授權此裝置", + "scanQr": "或用手機掃描此 QR 碼", + "openBrowser": "開啟瀏覽器", + "copyUrl": "複製 URL", + "waiting": "等待授權...", + "timeRemaining": "代碼將在 {{time}} 後過期", + "success": "登入成功!", + "authenticatedAs": "已驗證為 {{email}}", + "error": "驗證錯誤", + "unknownError": "發生錯誤。請重試。", + "cancel": "取消", + "retry": "重試", + "tryAgain": "重試", + "cancelled": "驗證已取消", + "initiating": "正在啟動驗證..." } } diff --git a/webview-ui/src/utils/kilocode/qrcode.ts b/webview-ui/src/utils/kilocode/qrcode.ts new file mode 100644 index 00000000000..b4394b7590e --- /dev/null +++ b/webview-ui/src/utils/kilocode/qrcode.ts @@ -0,0 +1,41 @@ +import QRCode from "qrcode" + +interface QRCodeOptions { + width?: number + margin?: number + color?: { + dark?: string + light?: string + } +} + +const DEFAULT_OPTIONS = { + width: 200, + margin: 2, + color: { + dark: "#000000", + light: "#FFFFFF", + }, +} as const + +function buildQRCodeOptions(options?: QRCodeOptions) { + return { + width: options?.width ?? DEFAULT_OPTIONS.width, + margin: options?.margin ?? DEFAULT_OPTIONS.margin, + color: { + dark: options?.color?.dark ?? DEFAULT_OPTIONS.color.dark, + light: options?.color?.light ?? DEFAULT_OPTIONS.color.light, + }, + } +} + +export async function generateQRCode(text: string, options?: QRCodeOptions): Promise { + return QRCode.toDataURL(text, buildQRCodeOptions(options)) +} + +export async function generateQRCodeSVG(text: string, options?: QRCodeOptions): Promise { + return QRCode.toString(text, { + type: "svg", + ...buildQRCodeOptions(options), + }) +}