Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/logout-keeps-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Logging out or removing the active provider no longer closes the current session.
11 changes: 10 additions & 1 deletion apps/kimi-code/src/tui/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,10 @@ export async function handleLogoutCommand(host: SlashCommandHost): Promise<void>
}

if (target === currentProvider) {
// Keep the session: only the provider credential is gone. The next turn
// fails with model.not_configured until the user logs in again or picks
// another model.
Comment thread
liruifengv marked this conversation as resolved.
await host.authFlow.refreshConfigAfterLogout();
await host.authFlow.clearActiveSessionAfterLogout();
} else {
const updated = await host.harness.getConfig({ reload: true });
host.setAppState({
Expand All @@ -253,5 +255,12 @@ export async function handleLogoutCommand(host: SlashCommandHost): Promise<void>

host.track('logout', { provider: target });
const label = target === DEFAULT_OAUTH_PROVIDER_NAME ? PRODUCT_NAME : target;
if (target === currentProvider) {
host.showStatus(
`Logged out from ${label}. Current model is unavailable — /login or /model to continue.`,
Comment thread
liruifengv marked this conversation as resolved.
'warning',
);
return;
}
host.showStatus(`Logged out from ${label}.`);
}
31 changes: 19 additions & 12 deletions apps/kimi-code/src/tui/commands/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,36 @@ async function handleProviderManagerDeleteSource(
}

async function handleProviderDelete(host: SlashCommandHost, providerId: string): Promise<void> {
const activeProvider =
host.state.appState.availableModels[host.state.appState.model]?.provider;

if (providerId === DEFAULT_OAUTH_PROVIDER_NAME) {
await host.harness.auth.logout(DEFAULT_OAUTH_PROVIDER_NAME);
// Drop the process-wide region cache with the credential: derived
// endpoints (updates, marketplace, site links, telemetry) must fall back
// to the marker/default profile, not the logged-out region.
refreshKimiRegion();
await host.authFlow.refreshConfigAfterLogout();
await host.authFlow.clearActiveSessionAfterLogout();
return;
} else {
await host.harness.removeProvider(providerId);
}

const activeProvider =
host.state.appState.availableModels[host.state.appState.model]?.provider;
const config = await host.harness.removeProvider(providerId);
if (activeProvider === providerId) {
// Keep the session, mirroring /logout: only the model display is cleared;
// the next turn fails with model.not_configured until the user logs in
// again or picks another model.
await host.authFlow.refreshConfigAfterLogout();
await host.authFlow.clearActiveSessionAfterLogout();
} else {
host.setAppState({
availableProviders: config.providers ?? {},
availableModels: config.models ?? {},
});
host.showStatus(
`Provider "${providerId}" was used by the current model — /login or /model to continue.`,
'warning',
);
return;
}

const updated = await host.harness.getConfig({ reload: true });
host.setAppState({
availableProviders: updated.providers ?? {},
availableModels: updated.models ?? {},
});
}

async function handleProviderAdd(host: SlashCommandHost): Promise<void> {
Expand Down
17 changes: 4 additions & 13 deletions apps/kimi-code/src/tui/controllers/auth-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export interface AuthFlowHost {
resetSessionRuntime(): void;
setSession(session: Session): Promise<void>;
syncRuntimeState(session?: Session): Promise<void>;
closeSession(reason: string): Promise<void>;
appendStartupNotice(extra: string): void;
hydrateLazyConfigDefaults(): Promise<void>;
readonly sessionEventHandler: SessionEventHandler;
Expand Down Expand Up @@ -83,6 +82,10 @@ export class AuthFlowController {
if (effort !== undefined) {
await host.session.setThinking(effort);
}
// Logging out with the session retained zeroed the footer's context
// counters; resync from the live session even when setModel was a
// no-op (same alias), so contextTokens/contextUsage are accurate again.
await host.syncRuntimeState(host.session);
return;
}

Expand Down Expand Up @@ -134,18 +137,6 @@ export class AuthFlowController {
void host.refreshPluginCommands(host.session);
}

async clearActiveSessionAfterLogout(): Promise<void> {
await this.host.closeSession('logged out');
this.host.resetSessionRuntime();
this.host.setAppState({
sessionId: '',
model: '',
sessionTitle: null,
});
await this.host.refreshSkillCommands();
await this.host.refreshPluginCommands();
}

async refreshConfigAfterLogin(): Promise<void> {
const { host } = this;
const config = await host.harness.getConfig({ reload: true });
Expand Down
52 changes: 48 additions & 4 deletions apps/kimi-code/test/tui/kimi-tui-startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1904,7 +1904,7 @@ describe('KimiTUI startup', () => {
}
});

it('tracks logout after managed credentials and session state are cleared', async () => {
it('keeps the session and clears model state when logging out the current provider', async () => {
const session = makeSession();
const harness = makeHarness(session, {
getConfig: vi.fn(async () => ({
Expand All @@ -1926,17 +1926,21 @@ describe('KimiTUI startup', () => {

await expect(driver.init()).resolves.toBe(false);
harness.track.mockClear();
const showStatus = vi.spyOn(driver as any, 'showStatus').mockImplementation(() => {});

vi.mocked(promptLogoutProviderSelection).mockResolvedValue('managed:kimi-code');
await handleLogoutCommand(driver as any);

expect(harness.auth.logout).toHaveBeenCalledWith('managed:kimi-code');
expect(session.close).toHaveBeenCalledOnce();
expect(session.close).not.toHaveBeenCalled();
expect(driver.state.appState).toMatchObject({
sessionId: '',
sessionId: 'ses-1',
model: '',
sessionTitle: null,
});
expect(showStatus).toHaveBeenCalledWith(
'Logged out from Kimi Code. Current model is unavailable — /login or /model to continue.',
'warning',
);
expect(harness.track).toHaveBeenCalledWith('logout', { provider: 'managed:kimi-code' });
});

Expand Down Expand Up @@ -1981,6 +1985,46 @@ describe('KimiTUI startup', () => {
expect(harness.track).toHaveBeenCalledWith('logout', { provider: 'openai' });
});

it('restores the retained session counters when logging back in after logout', async () => {
const session = makeSession();
const harness = makeHarness(session, {
getConfig: vi.fn(async () => ({
defaultModel: 'k2',
models: {
k2: { provider: 'managed:kimi-code', model: 'moonshot-v1', maxContextSize: 100 },
},
providers: { 'managed:kimi-code': { type: 'kimi' } },
})),
auth: {
status: vi.fn(async () => ({
providers: [{ providerName: 'managed:kimi-code', hasToken: true }],
})),
login: vi.fn(async () => {}),
logout: vi.fn(),
getManagedUsage: vi.fn(),
},
});
const driver = makeDriver(harness, makeStartupInput());

await expect(driver.init()).resolves.toBe(false);

vi.mocked(promptLogoutProviderSelection).mockResolvedValue('managed:kimi-code');
await handleLogoutCommand(driver as any);
expect(driver.state.appState).toMatchObject({ model: '', contextTokens: 0 });

vi.mocked(promptPlatformSelection).mockResolvedValue('kimi-code');
await handleLoginCommand(driver as any);

expect(session.setModel).toHaveBeenCalledWith('k2');
expect(driver.state.appState).toMatchObject({
sessionId: 'ses-1',
model: 'k2',
contextTokens: 10,
maxContextTokens: 100,
contextUsage: 0.1,
});
});

it('can log out a stale managed entry even after the OAuth token is gone', async () => {
const session = makeSession();
const harness = makeHarness(session, {
Expand Down
Loading