diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 75ab9751ce3..a728e472eff 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -190,6 +190,11 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "logout": await this.handleLogout() break + case "setOrganization": + if (typeof message.organizationId === "string" || message.organizationId === null) { + await this.handleSetOrganization(message.organizationId) + } + break case "refreshProfile": await this.handleRefreshProfile() break @@ -926,6 +931,11 @@ export class KiloProvider implements vscode.WebviewViewProvider { const profileData = await this.httpClient.getProfile() this.postMessage({ type: "profileData", data: profileData }) this.postMessage({ type: "deviceAuthComplete" }) + + // Step 5: If user has organizations, navigate to profile view so they can pick one + if (profileData?.profile.organizations && profileData.profile.organizations.length > 0) { + this.postMessage({ type: "navigate", view: "profile" }) + } } catch (error) { if (attempt !== this.loginAttempt) { return @@ -937,6 +947,41 @@ export class KiloProvider implements vscode.WebviewViewProvider { } } + /** + * Handle organization switch request from the webview. + * Persists the selection and refreshes profile + providers since both change with org context. + */ + private async handleSetOrganization(organizationId: string | null): Promise { + const client = this.httpClient + if (!client) { + return + } + + console.log("[Kilo New] KiloProvider: Switching organization:", organizationId ?? "personal") + try { + await client.setOrganization(organizationId) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to switch organization:", error) + // Re-fetch current profile to reset webview state (clears switching indicator) + const profileData = await client.getProfile() + this.postMessage({ type: "profileData", data: profileData }) + return + } + + // Org switch succeeded — refresh profile and providers independently (best-effort) + try { + const profileData = await client.getProfile() + this.postMessage({ type: "profileData", data: profileData }) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to refresh profile after org switch:", error) + } + try { + await this.fetchAndSendProviders() + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to refresh providers after org switch:", error) + } + } + /** * Handle logout request from the webview. */ diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts index 2deba34f723..d35e48a84be 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -313,6 +313,14 @@ export class HttpClient { } } + /** + * Switch the active organization. + * Pass null to switch back to personal account. + */ + async setOrganization(organizationId: string | null): Promise { + await this.request("POST", "/kilo/organization", { organizationId }) + } + // ============================================ // FIM Completion Methods // ============================================ diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index c90a9881d58..c16857461ef 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -18,6 +18,7 @@ import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2" import "./styles/chat.css" type ViewType = "newTask" | "marketplace" | "history" | "profile" | "settings" +const VALID_VIEWS = new Set(["newTask", "marketplace", "history", "profile", "settings"]) const DummyView: Component<{ title: string }> = (props) => { return ( @@ -119,6 +120,10 @@ const AppContent: Component = () => { console.log("[Kilo New] App: 🎬 action:", message.action) handleViewAction(message.action) } + if (message?.type === "navigate" && message.view && VALID_VIEWS.has(message.view)) { + console.log("[Kilo New] App: 🧭 navigate:", message.view) + setCurrentView(message.view as ViewType) + } } window.addEventListener("message", handler) onCleanup(() => window.removeEventListener("message", handler)) diff --git a/packages/kilo-vscode/webview-ui/src/components/ProfileView.tsx b/packages/kilo-vscode/webview-ui/src/components/ProfileView.tsx index f52643532c3..5bea5f827d2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/ProfileView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/ProfileView.tsx @@ -1,6 +1,7 @@ -import { Component, Show } from "solid-js" +import { Component, Show, createSignal, createMemo, createEffect } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { Card } from "@kilocode/kilo-ui/card" +import { Select } from "@kilocode/kilo-ui/select" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useVSCode } from "../context/vscode" import { useLanguage } from "../context/language" @@ -19,9 +20,56 @@ const formatBalance = (amount: number): string => { return `$${amount.toFixed(2)}` } +const PERSONAL = "personal" + +interface OrgOption { + value: string + label: string + description?: string +} + const ProfileView: Component = (props) => { const vscode = useVSCode() const language = useLanguage() + const [target, setTarget] = createSignal(null) + + // Reset pending target whenever profileData changes (success or failure both send a fresh profile) + createEffect(() => { + props.profileData // track + setTarget(null) + }) + + const switching = createMemo(() => { + const t = target() + if (t === null) return false + const current = props.profileData?.currentOrgId ?? PERSONAL + return current !== t + }) + + const orgOptions = createMemo(() => { + const orgs = props.profileData?.profile.organizations ?? [] + if (orgs.length === 0) return [] + return [ + { value: PERSONAL, label: "Personal Account" }, + ...orgs.map((org) => ({ value: org.id, label: org.name, description: org.role })), + ] + }) + + const currentOrg = createMemo(() => { + const id = props.profileData?.currentOrgId ?? PERSONAL + return orgOptions().find((o) => o.value === id) + }) + + const selectOrg = (option: OrgOption | undefined) => { + if (!option) return + const current = props.profileData?.currentOrgId ?? PERSONAL + if (option.value === current) return + setTarget(option.value) + vscode.postMessage({ + type: "setOrganization", + organizationId: option.value === PERSONAL ? null : option.value, + }) + } const handleLogin = () => { props.onLogin() @@ -126,6 +174,34 @@ const ProfileView: Component = (props) => {

+ {/* Organization selector */} + 0}> + +

+ Account +

+