From a641d3446b7e170c265f3224968feabd334ccb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 12 Feb 2026 16:52:31 +0100 Subject: [PATCH 1/7] feat(vscode): add org switching HTTP client method and message types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add setOrganization() to HttpClient for calling POST /kilo/organization. Add SetOrganizationRequest (webview→extension) and NavigateMessage (extension→webview) to the message type system. --- .../src/services/cli-backend/http-client.ts | 9 +++++++++ .../kilo-vscode/webview-ui/src/types/messages.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+) 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 51992e90c39..1bc5dbb7a72 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,15 @@ 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 }) + return true + } + // ============================================ // Auth Methods // ============================================ diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 0168a209918..d4d0d3a9466 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -400,6 +400,11 @@ export interface DeviceAuthCancelledMessage { type: "deviceAuthCancelled" } +export interface NavigateMessage { + type: "navigate" + view: string +} + export interface ProvidersLoadedMessage { type: "providersLoaded" providers: Record @@ -482,6 +487,7 @@ export type ExtensionMessage = | DeviceAuthCompleteMessage | DeviceAuthFailedMessage | DeviceAuthCancelledMessage + | NavigateMessage | ProvidersLoadedMessage | AgentsLoadedMessage | QuestionRequestMessage @@ -555,6 +561,11 @@ export interface CancelLoginRequest { type: "cancelLogin" } +export interface SetOrganizationRequest { + type: "setOrganization" + organizationId: string | null +} + export interface WebviewReadyRequest { type: "webviewReady" } @@ -637,6 +648,7 @@ export type WebviewMessage = | RefreshProfileRequest | OpenExternalRequest | CancelLoginRequest + | SetOrganizationRequest | WebviewReadyRequest | RequestProvidersMessage | CompactRequest From 99797f10a1bda248c8089a2f54ef8267f6bb498c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 12 Feb 2026 16:52:38 +0100 Subject: [PATCH 2/7] feat(vscode): handle org switching and post-login org navigation Handle setOrganization message to persist org selection and refresh profile + providers. After login, auto-navigate to profile view when the user belongs to organizations so they can pick one. --- packages/kilo-vscode/src/KiloProvider.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 71fb997b592..c728e830cd9 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -188,6 +188,9 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "logout": await this.handleLogout() break + case "setOrganization": + await this.handleSetOrganization(message.organizationId) + break case "refreshProfile": await this.handleRefreshProfile() break @@ -897,6 +900,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 @@ -908,6 +916,24 @@ 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 { + if (!this.httpClient) { + return + } + + console.log("[Kilo New] KiloProvider: 🏢 Switching organization:", organizationId ?? "personal") + await this.httpClient.setOrganization(organizationId) + + // Refresh profile (balance changes per org) and providers (models differ per org) + const profileData = await this.httpClient.getProfile() + this.postMessage({ type: "profileData", data: profileData }) + await this.fetchAndSendProviders() + } + /** * Handle logout request from the webview. */ From 74478272bc70f598ce019c4c3a0adfd6801ca9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 12 Feb 2026 16:52:44 +0100 Subject: [PATCH 3/7] feat(vscode): add org selector dropdown to profile view Show a dropdown in the profile view for users with organizations, allowing them to switch between personal account and org accounts. Handle navigate messages in App.tsx for post-login auto-navigation. --- packages/kilo-vscode/webview-ui/src/App.tsx | 4 ++ .../webview-ui/src/components/ProfileView.tsx | 69 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index c90a9881d58..d6e675bd15f 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -119,6 +119,10 @@ const AppContent: Component = () => { console.log("[Kilo New] App: 🎬 action:", message.action) handleViewAction(message.action) } + if (message?.type === "navigate" && message.view) { + console.log("[Kilo New] App: 🧭 navigate:", message.view) + setCurrentView(message.view) + } } 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..f06c034c88e 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 } 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,47 @@ const formatBalance = (amount: number): string => { return `$${amount.toFixed(2)}` } +interface OrgOption { + value: string | null + label: string + description?: string +} + const ProfileView: Component = (props) => { const vscode = useVSCode() const language = useLanguage() + const [switching, setSwitching] = createSignal(false) + + const orgOptions = createMemo(() => { + const orgs = props.profileData?.profile.organizations ?? [] + if (orgs.length === 0) { + return [] + } + return [ + { value: null, label: "Personal Account" }, + ...orgs.map((org) => ({ value: org.id, label: org.name, description: org.role })), + ] + }) + + const currentOrg = createMemo(() => { + const id = props.profileData?.currentOrgId ?? null + return orgOptions().find((o) => o.value === id) + }) + + const handleSelectOrg = (option: OrgOption) => { + if (option.value === (props.profileData?.currentOrgId ?? null)) { + return + } + setSwitching(true) + vscode.postMessage({ type: "setOrganization", organizationId: option.value }) + // switching state resets when new profileData arrives + } + + // Reset switching state when profile data changes (org switch completed) + createMemo(() => { + props.profileData + setSwitching(false) + }) const handleLogin = () => { props.onLogin() @@ -126,6 +165,34 @@ const ProfileView: Component = (props) => {

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

+ Account +

+ o.value ?? "personal"} + value={(o) => o.value} label={(o) => o.label} - onSelect={handleSelectOrg} + onSelect={selectOrg} variant="secondary" size="small" triggerVariant="settings" From 31442115aa58c89cc0b67a332dea74f8159c4eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 12 Feb 2026 18:47:42 +0100 Subject: [PATCH 6/7] fix(vscode): address org selector review feedback - Reset target signal on profileData change to prevent stuck switching state - Narrow try scope in handleSetOrganization for proper partial-failure handling - Add type parameter to setOrganization request for type safety - Validate navigate.view against allow-list before setting UI state --- packages/kilo-vscode/src/KiloProvider.ts | 19 ++++++++++++++----- .../src/services/cli-backend/http-client.ts | 2 +- packages/kilo-vscode/webview-ui/src/App.tsx | 5 +++-- .../webview-ui/src/components/ProfileView.tsx | 8 +++++++- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index b1325591a1e..3202b510c96 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -928,16 +928,25 @@ export class KiloProvider implements vscode.WebviewViewProvider { console.log("[Kilo New] KiloProvider: Switching organization:", organizationId ?? "personal") try { await this.httpClient.setOrganization(organizationId) - - // Refresh profile (balance changes per org) and providers (models differ per org) - const profileData = await this.httpClient.getProfile() - this.postMessage({ type: "profileData", data: profileData }) - await this.fetchAndSendProviders() } 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 this.httpClient.getProfile() this.postMessage({ type: "profileData", data: profileData }) + return + } + + // Org switch succeeded — refresh profile and providers independently (best-effort) + try { + const profileData = await this.httpClient.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) } } 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 e4240bde030..2ce98921548 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -318,7 +318,7 @@ export class HttpClient { * Pass null to switch back to personal account. */ async setOrganization(organizationId: string | null): Promise { - await this.request("POST", "/kilo/organization", { organizationId }) + await this.request("POST", "/kilo/organization", { organizationId }) } // ============================================ diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index d6e675bd15f..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,9 +120,9 @@ const AppContent: Component = () => { console.log("[Kilo New] App: 🎬 action:", message.action) handleViewAction(message.action) } - if (message?.type === "navigate" && message.view) { + if (message?.type === "navigate" && message.view && VALID_VIEWS.has(message.view)) { console.log("[Kilo New] App: 🧭 navigate:", message.view) - setCurrentView(message.view) + setCurrentView(message.view as ViewType) } } window.addEventListener("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 0ef27decf23..5bea5f827d2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/ProfileView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/ProfileView.tsx @@ -1,4 +1,4 @@ -import { Component, Show, createSignal, createMemo } 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" @@ -33,6 +33,12 @@ const ProfileView: Component = (props) => { 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 From 5df2699fb37b0636a1f02d54f4f51b63f4b73682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 12 Feb 2026 18:58:08 +0100 Subject: [PATCH 7/7] fix(vscode): validate org message payload and cache httpClient getter --- packages/kilo-vscode/src/KiloProvider.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 3202b510c96..a53f000c2c1 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -189,7 +189,9 @@ export class KiloProvider implements vscode.WebviewViewProvider { await this.handleLogout() break case "setOrganization": - await this.handleSetOrganization(message.organizationId) + if (typeof message.organizationId === "string" || message.organizationId === null) { + await this.handleSetOrganization(message.organizationId) + } break case "refreshProfile": await this.handleRefreshProfile() @@ -921,24 +923,25 @@ export class KiloProvider implements vscode.WebviewViewProvider { * Persists the selection and refreshes profile + providers since both change with org context. */ private async handleSetOrganization(organizationId: string | null): Promise { - if (!this.httpClient) { + const client = this.httpClient + if (!client) { return } console.log("[Kilo New] KiloProvider: Switching organization:", organizationId ?? "personal") try { - await this.httpClient.setOrganization(organizationId) + 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 this.httpClient.getProfile() + 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 this.httpClient.getProfile() + 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)