Skip to content

Commit 03e66cb

Browse files
committed
fix(kimi-code): keep /logout usable for stale managed config and unrelated providers
- Surface the Kimi Code OAuth entry in the picker whenever config.providers still references it, not only when a live token is present. auth.logout cleans the config either way, so stale entries can no longer become unreachable through /logout. - When the picked provider is not the one backing the current model, just refresh the provider/model listing and leave the session and model selection alone. Full teardown stays for the case where the active provider was the one being removed.
1 parent a922ea7 commit 03e66cb

2 files changed

Lines changed: 107 additions & 9 deletions

File tree

‎apps/kimi-code/src/tui/kimi-tui.ts‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5375,23 +5375,28 @@ export class KimiTUI {
53755375
}
53765376

53775377
// Handles the /logout command. Lists every credential currently held — the
5378-
// Kimi Code OAuth token (when present) plus each configured API-key provider
5379-
// — and lets the user pick which one to drop. managed:kimi-code is also
5380-
// written into config.providers, so it must be filtered out of the API-key
5381-
// list to avoid showing twice; OAuth tokens go through auth.logout for
5382-
// proper revocation, everything else through removeProvider.
5378+
// Kimi Code OAuth token (or a stale config entry for it) plus each configured
5379+
// API-key provider — and lets the user pick which one to drop. OAuth tokens
5380+
// go through auth.logout for proper revocation, everything else through
5381+
// removeProvider.
53835382
private async handleLogoutCommand(): Promise<void> {
53845383
const oauthStatus = await this.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME);
5385-
const hasOAuth = oauthStatus.providers.some(
5384+
const hasOAuthToken = oauthStatus.providers.some(
53865385
(p) => p.providerName === DEFAULT_OAUTH_PROVIDER_NAME && p.hasToken,
53875386
);
53885387
const config = await this.harness.getConfig();
5388+
// Offer the managed provider whenever something points at it — either a
5389+
// live OAuth token or a stale providers[] entry left over from a previous
5390+
// login. auth.logout cleans the config regardless of whether the token
5391+
// is still present, so this avoids leaving residue with no way to reach it.
5392+
const hasManagedRemnant =
5393+
hasOAuthToken || config.providers[DEFAULT_OAUTH_PROVIDER_NAME] !== undefined;
53895394
const apiKeyProviderIds = Object.keys(config.providers ?? {})
53905395
.filter((id) => id !== DEFAULT_OAUTH_PROVIDER_NAME)
53915396
.toSorted();
53925397

53935398
const options: ChoiceOption[] = [];
5394-
if (hasOAuth) {
5399+
if (hasManagedRemnant) {
53955400
options.push({
53965401
value: DEFAULT_OAUTH_PROVIDER_NAME,
53975402
label: PRODUCT_NAME,
@@ -5423,8 +5428,22 @@ export class KimiTUI {
54235428
} else {
54245429
await this.harness.removeProvider(target);
54255430
}
5426-
await this.refreshConfigAfterLogout();
5427-
await this.clearActiveSessionAfterLogout();
5431+
5432+
if (target === currentProvider) {
5433+
// The active session is backed by the provider we just removed, so it
5434+
// can no longer make requests — tear it down along with the model state.
5435+
await this.refreshConfigAfterLogout();
5436+
await this.clearActiveSessionAfterLogout();
5437+
} else {
5438+
// Refresh provider/model listings so the picker reflects the change,
5439+
// but leave the user's current session running.
5440+
const updated = await this.harness.getConfig({ reload: true });
5441+
this.setAppState({
5442+
availableModels: updated.models ?? {},
5443+
availableProviders: updated.providers ?? {},
5444+
});
5445+
}
5446+
54285447
this.track('logout', { provider: target });
54295448
const label = target === DEFAULT_OAUTH_PROVIDER_NAME ? PRODUCT_NAME : target;
54305449
this.showStatus(`Logged out from ${label}.`);

‎apps/kimi-code/test/tui/kimi-tui-startup.test.ts‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,12 @@ describe("KimiTUI startup", () => {
568568
it("tracks logout after managed credentials and session state are cleared", async () => {
569569
const session = makeSession();
570570
const harness = makeHarness(session, {
571+
getConfig: vi.fn(async () => ({
572+
models: {
573+
k2: { provider: "managed:kimi-code", model: "moonshot-v1", maxContextSize: 100 },
574+
},
575+
providers: { "managed:kimi-code": { type: "kimi" } },
576+
})),
571577
auth: {
572578
status: vi.fn(async () => ({
573579
providers: [{ providerName: "managed:kimi-code", hasToken: true }],
@@ -597,6 +603,79 @@ describe("KimiTUI startup", () => {
597603
expect(harness.track).toHaveBeenCalledWith("logout", { provider: "managed:kimi-code" });
598604
});
599605

606+
it("keeps the active session when logging out a different provider", async () => {
607+
const session = makeSession();
608+
const removeProvider = vi.fn(async () => {});
609+
const harness = makeHarness(session, {
610+
getConfig: vi.fn(async () => ({
611+
models: {
612+
k2: { provider: "managed:kimi-code", model: "moonshot-v1", maxContextSize: 100 },
613+
},
614+
providers: {
615+
"managed:kimi-code": { type: "kimi" },
616+
openai: { type: "openai", baseUrl: "https://api.openai.com/v1" },
617+
},
618+
})),
619+
removeProvider,
620+
auth: {
621+
status: vi.fn(async () => ({
622+
providers: [{ providerName: "managed:kimi-code", hasToken: true }],
623+
})),
624+
login: vi.fn(async () => {}),
625+
logout: vi.fn(),
626+
getManagedUsage: vi.fn(),
627+
},
628+
});
629+
const driver = makeDriver(harness, makeStartupInput());
630+
631+
await expect(driver.init()).resolves.toBe(false);
632+
harness.track.mockClear();
633+
634+
vi.spyOn(driver as any, "promptLogoutProviderSelection").mockResolvedValue("openai");
635+
await driver.handleLogoutCommand();
636+
637+
expect(removeProvider).toHaveBeenCalledWith("openai");
638+
expect(harness.auth.logout).not.toHaveBeenCalled();
639+
expect(session.close).not.toHaveBeenCalled();
640+
expect(driver.state.appState).toMatchObject({
641+
sessionId: "ses-1",
642+
model: "k2",
643+
});
644+
expect(harness.track).toHaveBeenCalledWith("logout", { provider: "openai" });
645+
});
646+
647+
it("can log out a stale managed entry even after the OAuth token is gone", async () => {
648+
const session = makeSession();
649+
const harness = makeHarness(session, {
650+
getConfig: vi.fn(async () => ({
651+
models: {
652+
k2: { provider: "managed:kimi-code", model: "moonshot-v1", maxContextSize: 100 },
653+
},
654+
providers: { "managed:kimi-code": { type: "kimi" } },
655+
})),
656+
auth: {
657+
// Token gone (e.g. credentials file deleted) but the managed entry
658+
// is still sitting in config.providers.
659+
status: vi.fn(async () => ({
660+
providers: [{ providerName: "managed:kimi-code", hasToken: false }],
661+
})),
662+
login: vi.fn(async () => {}),
663+
logout: vi.fn(),
664+
getManagedUsage: vi.fn(),
665+
},
666+
});
667+
const driver = makeDriver(harness, makeStartupInput());
668+
669+
await expect(driver.init()).resolves.toBe(false);
670+
671+
vi.spyOn(driver as any, "promptLogoutProviderSelection").mockResolvedValue(
672+
"managed:kimi-code",
673+
);
674+
await driver.handleLogoutCommand();
675+
676+
expect(harness.auth.logout).toHaveBeenCalledWith("managed:kimi-code");
677+
});
678+
600679
it("starts TUI without replaying when --continue needs OAuth login", async () => {
601680
const harness = makeHarness(makeSession(), {
602681
listSessions: vi.fn(async () => [{ id: "ses-latest" }]),

0 commit comments

Comments
 (0)