Merge pull request #297 from Kilo-Org/feat/vscode-org-selector

feat(vscode): add org selector dropdown to profile view
This commit is contained in:
Igor Šćekić
2026-02-12 19:03:25 +01:00
committed by GitHub
5 changed files with 147 additions and 1 deletions
+45
View File
@@ -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<void> {
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.
*/
@@ -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<void> {
await this.request<boolean>("POST", "/kilo/organization", { organizationId })
}
// ============================================
// FIM Completion Methods
// ============================================
@@ -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<string>(["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))
@@ -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<ProfileViewProps> = (props) => {
const vscode = useVSCode()
const language = useLanguage()
const [target, setTarget] = createSignal<string | null>(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<OrgOption[]>(() => {
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<ProfileViewProps> = (props) => {
</p>
</Card>
{/* Organization selector */}
<Show when={orgOptions().length > 0}>
<Card>
<p
style={{
"font-size": "11px",
"text-transform": "uppercase",
"letter-spacing": "0.5px",
color: "var(--vscode-descriptionForeground)",
margin: "0 0 8px 0",
}}
>
Account
</p>
<Select
options={orgOptions()}
current={currentOrg()}
value={(o) => o.value}
label={(o) => o.label}
onSelect={selectOrg}
variant="secondary"
size="small"
triggerVariant="settings"
disabled={switching()}
/>
</Card>
</Show>
{/* Balance */}
<Show when={data().balance}>
{(balance) => (
@@ -400,6 +400,11 @@ export interface DeviceAuthCancelledMessage {
type: "deviceAuthCancelled"
}
export interface NavigateMessage {
type: "navigate"
view: "newTask" | "marketplace" | "history" | "profile" | "settings"
}
export interface ProvidersLoadedMessage {
type: "providersLoaded"
providers: Record<string, Provider>
@@ -497,6 +502,7 @@ export type ExtensionMessage =
| DeviceAuthCompleteMessage
| DeviceAuthFailedMessage
| DeviceAuthCancelledMessage
| NavigateMessage
| ProvidersLoadedMessage
| AgentsLoadedMessage
| AutocompleteSettingsLoadedMessage
@@ -572,6 +578,11 @@ export interface CancelLoginRequest {
type: "cancelLogin"
}
export interface SetOrganizationRequest {
type: "setOrganization"
organizationId: string | null
}
export interface WebviewReadyRequest {
type: "webviewReady"
}
@@ -674,6 +685,7 @@ export type WebviewMessage =
| RefreshProfileRequest
| OpenExternalRequest
| CancelLoginRequest
| SetOrganizationRequest
| WebviewReadyRequest
| RequestProvidersMessage
| CompactRequest