mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user