diff --git a/.changeset/selected-organization-default.md b/.changeset/selected-organization-default.md index 1f4c262b29..78a9cec07c 100644 --- a/.changeset/selected-organization-default.md +++ b/.changeset/selected-organization-default.md @@ -3,4 +3,4 @@ "@kilocode/kilo-gateway": patch --- -Use the cloud-selected organization as the active Kilo account when provided. +Use cloud account preferences to select the active Kilo organization and hide unavailable personal accounts. diff --git a/packages/kilo-gateway/src/api/profile.ts b/packages/kilo-gateway/src/api/profile.ts index ae7d46eeed..fb3413ddf8 100644 --- a/packages/kilo-gateway/src/api/profile.ts +++ b/packages/kilo-gateway/src/api/profile.ts @@ -26,6 +26,7 @@ export async function fetchProfile(token: string): Promise { name?: string organizations?: Organization[] selectedOrganizationId?: string | null + hasPersonalAccount?: boolean | null } // Backend returns { user: { email, name, ... }, organizations } // Transform to flat KilocodeProfile structure @@ -34,6 +35,7 @@ export async function fetchProfile(token: string): Promise { name: data.user?.name ?? data.name, organizations: data.organizations, selectedOrganizationId: data.selectedOrganizationId ?? undefined, + hasPersonalAccount: data.hasPersonalAccount ?? undefined, } } diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index cc60ecc62c..177674cd18 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -102,6 +102,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { name: z.string().optional(), organizations: z.array(Organization).optional(), selectedOrganizationId: z.string().optional(), + hasPersonalAccount: z.boolean().optional(), }) const Balance = z.object({ diff --git a/packages/kilo-gateway/src/tui/helpers.ts b/packages/kilo-gateway/src/tui/helpers.ts index 6884312e49..cc7a72311a 100644 --- a/packages/kilo-gateway/src/tui/helpers.ts +++ b/packages/kilo-gateway/src/tui/helpers.ts @@ -56,6 +56,7 @@ export function formatProfileInfo( export function getOrganizationOptions( organizations: Organization[], currentOrgId?: string, + hasPersonalAccount = true, ): Array<{ title: string value: string | null @@ -63,12 +64,16 @@ export function getOrganizationOptions( category: string }> { return [ - { - title: "Personal Account", - value: null, - description: !currentOrgId ? "→ (current)" : undefined, - category: "Accounts", - }, + ...(hasPersonalAccount + ? [ + { + title: "Personal Account", + value: null, + description: !currentOrgId ? "→ (current)" : undefined, + category: "Accounts", + }, + ] + : []), ...organizations.map((org) => ({ title: org.name, value: org.id, diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index c9014f1bc2..3100b1e630 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -28,6 +28,7 @@ export interface KilocodeProfile { name?: string organizations?: Organization[] selectedOrganizationId?: string + hasPersonalAccount?: boolean } export interface KilocodeBalance { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt index b593cb5dba..5e02bbb881 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt @@ -143,6 +143,7 @@ internal fun profileDto(p: KiloProfile200Response): ProfileDto = ProfileDto( organizations = p.profile.organizations.orEmpty().map { org -> ProfileOrganizationDto(id = org.id, name = org.name, role = org.role) }, + hasPersonalAccount = p.profile.hasPersonalAccount ?: true, balance = p.balance?.let { ProfileBalanceDto(balance = it.balance) }, kiloPass = p.kiloPass?.let { ProfileKiloPassDto( diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt index 7ba64fec23..5cb922a1d3 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt @@ -790,6 +790,7 @@ class KiloBackendAppServiceTest { "profile":{ "email":"alice@test.com", "name":"Alice", + "hasPersonalAccount":false, "organizations":[{"id":"org_1","name":"Acme","role":"ADMIN"}] }, "balance":{"balance":42.5}, @@ -804,6 +805,7 @@ class KiloBackendAppServiceTest { assertEquals("alice@test.com", dto.profile?.email) assertEquals("Alice", dto.profile?.name) assertEquals("ADMIN", dto.profile?.organizations?.firstOrNull()?.role) + assertFalse(dto.profile?.hasPersonalAccount ?: true) assertEquals(42.5, dto.profile?.balance?.balance) assertEquals("org_1", dto.profile?.currentOrgId) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt index 82a0429911..7a05e01202 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt @@ -124,7 +124,8 @@ internal class SessionAccountOverlay( var layout = false val orgs = prof.organizations - val next = listOf(AccountChoice(null, KiloBundle.message("profile.personalAccount"))) + + val personal = prof.hasPersonalAccount + val next = (if (personal) listOf(AccountChoice(null, KiloBundle.message("profile.personalAccount"))) else emptyList()) + orgs.map { org -> AccountChoice(org.id, org.name) } if (next != choices) { choices = next @@ -133,7 +134,7 @@ internal class SessionAccountOverlay( if (currentOrgId != prof.currentOrgId) currentOrgId = prof.currentOrgId - val activeId = if (switching) target else prof.currentOrgId + val activeId = if (switching) target else prof.currentOrgId ?: if (personal) null else orgs.firstOrNull()?.id val active = choices.firstOrNull { it.org == activeId } ?: choices.firstOrNull() val title = "${active?.title ?: " "} ▾" if (picker.text != title) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt index 520aa26ae7..63fa8589d6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt @@ -331,12 +331,11 @@ internal class LoggedInProfileUi( @RequiresEdt private fun applyOrganizations(profile: ProfileDto) { val orgs = profile.organizations - val keys: List> = listOf(null to KiloBundle.message("profile.personalAccount")) + + val personal = profile.hasPersonalAccount + val keys: List> = (if (personal) listOf(null to KiloBundle.message("profile.personalAccount")) else emptyList()) + orgs.map { it.id to it.name } - val target = profile.currentOrgId - ?.let { id -> orgs.indexOfFirst { it.id == id }.takeIf { it >= 0 }?.plus(1) } - ?: 0 + val target = keys.indexOfFirst { it.first == profile.currentOrgId }.takeIf { it >= 0 } ?: 0 currentOrgId = profile.currentOrgId diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt index 4aaa851181..428f682fb6 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt @@ -42,10 +42,12 @@ class SessionAccountOverlayTest : SessionControllerTestBase() { organizations: List = emptyList(), balance: ProfileBalanceDto? = null, currentOrgId: String? = null, + hasPersonalAccount: Boolean = true, ) = ProfileDto( email = email, name = name, organizations = organizations, + hasPersonalAccount = hasPersonalAccount, balance = balance, currentOrgId = currentOrgId, ) @@ -106,6 +108,22 @@ class SessionAccountOverlayTest : SessionControllerTestBase() { } } + fun `test profile without personal account hides personal choice`() { + val acme = org("org_1", "Acme", "MEMBER") + val prof = profile( + email = "user@example.com", + organizations = listOf(acme), + currentOrgId = "org_1", + hasPersonalAccount = false, + ) + show(snap(prof)) + edt { + assertEquals("Acme", panel.accountTitle()) + assertEquals(1, panel.choiceCount()) + assertEquals(0, panel.selectedIndex()) + } + } + // --- test 4: programmatic update does not call select callback --- fun `test programmatic update does not call select callback`() { diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt index cbc72c6db7..9fe4d903c5 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt @@ -155,6 +155,7 @@ data class ProfileDto( val email: String, val name: String? = null, val organizations: List = emptyList(), + val hasPersonalAccount: Boolean = true, val balance: ProfileBalanceDto? = null, val kiloPass: ProfileKiloPassDto? = null, val currentOrgId: String? = null, diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index 99f52c80d3..9ff83a941f 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -32,6 +32,7 @@ export interface KilocodeProfile { name?: string organizations?: KilocodeOrganization[] selectedOrganizationId?: string + hasPersonalAccount?: boolean } export interface KilocodeBalance { diff --git a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx index 05c1cb1bcc..e108724703 100644 --- a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx @@ -65,20 +65,23 @@ const ProfileView: Component = (props) => { const orgOptions = createMemo(() => { const orgs = props.profileData?.profile.organizations ?? [] if (orgs.length === 0) return [] + const personal = props.profileData?.profile.hasPersonalAccount !== false return [ - { value: PERSONAL, label: language.t("profile.personalAccount") }, + ...(personal ? [{ value: PERSONAL, label: language.t("profile.personalAccount") }] : []), ...orgs.map((org) => ({ value: org.id, label: org.name, description: org.role })), ] }) const currentOrg = createMemo(() => { - const id = props.profileData?.currentOrgId ?? PERSONAL + const personal = props.profileData?.profile.hasPersonalAccount !== false + const id = props.profileData?.currentOrgId ?? (personal ? PERSONAL : orgOptions()[0]?.value) return orgOptions().find((o) => o.value === id) }) const selectOrg = (option: OrgOption | undefined) => { if (!option) return - const current = props.profileData?.currentOrgId ?? PERSONAL + const personal = props.profileData?.profile.hasPersonalAccount !== false + const current = props.profileData?.currentOrgId ?? (personal ? PERSONAL : orgOptions()[0]?.value) if (option.value === current) return setTarget(option.value) vscode.postMessage({ @@ -258,7 +261,13 @@ const ProfileView: Component = (props) => { {/* Kilo Pass is part of personal credits, so only show it on the personal account */} - + {(pass) => (
= (props) => { {/* No active Kilo Pass on the personal account — nudge to subscribe */} - +
= (props) => { const profile = () => server.profileData() const orgs = () => profile()?.profile.organizations ?? [] + const personal = () => profile()?.profile.hasPersonalAccount !== false const visible = () => !!profile() && orgs().length > 0 - const current = () => profile()?.currentOrgId ?? PERSONAL + const current = () => profile()?.currentOrgId ?? (personal() ? PERSONAL : (orgs()[0]?.id ?? PERSONAL)) const selected = createMemo(() => { const id = current() @@ -119,15 +120,17 @@ export const AccountSwitcher: Component<{ class?: string }> = (props) => {