Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
758ab5e
refactor(tui): extract TasksBrowserController from KimiTUI
liruifengv May 27, 2026
1462c33
refactor(tui): extract StreamingUIController from KimiTUI
liruifengv May 27, 2026
66a48fc
refactor(tui): extract SessionEventHandler from KimiTUI
liruifengv May 27, 2026
d3541ed
refactor(tui): extract SessionReplayRenderer from KimiTUI
liruifengv May 27, 2026
37bac0c
refactor(tui): extract slash command handlers from KimiTUI
liruifengv May 27, 2026
0071dfb
refactor(tui): extract AuthFlowController from KimiTUI
liruifengv May 27, 2026
f1dad39
refactor(tui): clean up dead imports after controller extraction
liruifengv May 27, 2026
513e829
refactor(tui): remove delegate methods and direct controller references
liruifengv May 27, 2026
fde7dca
refactor(tui): move pickers, config apply, info commands to slash-com…
liruifengv May 27, 2026
96b3c0e
refactor(tui): final cleanup — remove last delegates and dead imports
liruifengv May 27, 2026
f894463
refactor(tui): clean up dead imports and empty import blocks
liruifengv May 27, 2026
e71fd77
refactor(tui): inline last auth delegate methods
liruifengv May 27, 2026
0cec672
refactor(tui): move slash command dispatch to slash-commands controller
liruifengv May 27, 2026
c00ae61
refactor(tui): clean up 38 dead imports and 3 dead interfaces
liruifengv May 27, 2026
4b6214a
refactor(tui): deduplicate combineStartupNotice and isOAuthLoginRequi…
liruifengv May 27, 2026
8ac19c1
refactor(tui): move startup utils out of constant/kimi-tui
liruifengv May 27, 2026
7927e4b
refactor(tui): remove last session event/replay delegate methods
liruifengv May 27, 2026
8590bf1
refactor(tui): reduce TUIState fields by removing redundancy and merg…
liruifengv May 27, 2026
935d33e
refactor(tui): extract TUIState types and move streaming state into S…
liruifengv May 27, 2026
995ca18
refactor(tui): split slash-commands.ts by domain into 4 focused modules
liruifengv May 27, 2026
78b7990
refactor(tui): move command handlers from controllers/ to commands/
liruifengv May 27, 2026
19478a2
refactor(tui): move background/render-dedup state into SessionEventHa…
liruifengv May 27, 2026
cba8e6c
refactor(tui): encapsulate StreamingUIController internal state behin…
liruifengv May 27, 2026
0e69a0a
refactor(tui): extract EditorKeyboardController from KimiTUI
liruifengv May 27, 2026
0d3fdfd
refactor(tui): clean up kimi-tui.ts — strip noise comments, reorganiz…
liruifengv May 27, 2026
69953f5
refactor(tui): route TUIState field mutations through host methods
liruifengv May 27, 2026
27749de
merge origin/main — integrate 12 upstream commits into refactored TUI
liruifengv May 27, 2026
b191150
merge origin/main — integrate 4 upstream commits into refactored TUI
liruifengv May 28, 2026
ae1eeba
chore(tui): drop merge analysis docs, add changeset, fix 2 lint errors
liruifengv May 28, 2026
39e6289
chore(changeset): simplify wording
liruifengv May 28, 2026
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/refactor-tui-kimi-tui-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Refactor TUI code structure.
349 changes: 349 additions & 0 deletions apps/kimi-code/src/tui/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
import {
applyOpenPlatformConfig,
fetchOpenPlatformModels,
filterModelsByPrefix,
getOpenPlatformById,
OpenPlatformApiError,
type ManagedKimiCodeModelInfo,
type ManagedKimiConfigShape,
type OpenPlatformDefinition,
} from '@moonshot-ai/kimi-code-oauth';
import {
applyCatalogProvider,
catalogBaseUrl,
catalogProviderModels,
CatalogFetchError,
fetchCatalog,
inferWireType,
loadBuiltInCatalog,
log,
type Catalog,
} from '@moonshot-ai/kimi-code-sdk';

import { BUILT_IN_CATALOG_JSON } from '../../built-in-catalog';
import type { ChoiceOption } from '../components/dialogs/choice-picker';
import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '../constant/kimi-tui';
import { resolveConnectCatalogRequest } from '../utils/connect-catalog';
import { formatErrorMessage } from '../utils/event-payload';
import type { LoginProgressSpinnerHandle } from '../types';
import {
promptApiKey,
promptCatalogProviderSelection,
promptLogoutProviderSelection,
promptModelSelectionForCatalog,
promptModelSelectionForOpenPlatform,
promptPlatformSelection,
} from './prompts';
import type { SlashCommandHost } from './dispatch';

// ---------------------------------------------------------------------------
// Auth: login / logout / connect
// ---------------------------------------------------------------------------

export async function handleLoginCommand(host: SlashCommandHost): Promise<void> {
const platformId = await promptPlatformSelection(host);
if (platformId === undefined) return;

if (platformId === 'kimi-code') {
await handleKimiCodeOAuthLogin(host);
return;
}

const platform = getOpenPlatformById(platformId);
if (platform === undefined) return;
await handleOpenPlatformLogin(host, platform);
}

async function handleKimiCodeOAuthLogin(host: SlashCommandHost): Promise<void> {
const status = await host.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME);
const alreadyLoggedIn = status.providers.some(
(provider) => provider.providerName === DEFAULT_OAUTH_PROVIDER_NAME && provider.hasToken,
);

let spinner: LoginProgressSpinnerHandle | undefined;
const controller = new AbortController();
const cancelLogin = (): void => {
controller.abort();
};
host.cancelInFlight = cancelLogin;
try {
await host.harness.auth.login(DEFAULT_OAUTH_PROVIDER_NAME, {
signal: controller.signal,
onDeviceCode: (data) => {
spinner = host.showLoginAuthorizationPrompt(data);
},
});
spinner?.stop({ ok: true, label: 'Logged in.' });
spinner = undefined;
try {
await host.authFlow.refreshConfigAfterLogin();
} catch (refreshError) {
const message = formatErrorMessage(refreshError);
host.showError(`Authentication successful, but failed to refresh config: ${message}`);
return;
}
host.track('login', {
provider: DEFAULT_OAUTH_PROVIDER_NAME,
already_logged_in: alreadyLoggedIn,
});
if (alreadyLoggedIn) {
host.showStatus('Already logged in. Model configuration refreshed.');
}
} catch (error) {
const cancelled = controller.signal.aborted;
spinner?.stop({
ok: false,
label: cancelled ? 'Login cancelled.' : 'Login failed.',
});
spinner = undefined;
if (cancelled) return;
log.warn('login failed', {
providerName: DEFAULT_OAUTH_PROVIDER_NAME,
alreadyLoggedIn,
sessionId: host.session?.id,
error,
});
const message = formatErrorMessage(error);
host.showError(`Login failed: ${message}`);
} finally {
if (host.cancelInFlight === cancelLogin) {
host.cancelInFlight = undefined;
}
}
}

async function handleOpenPlatformLogin(
host: SlashCommandHost,
platform: OpenPlatformDefinition,
): Promise<void> {
const apiKey = await promptApiKey(host, platform.name);
if (apiKey === undefined) return;

const controller = new AbortController();
const cancelLogin = (): void => {
controller.abort();
};
host.cancelInFlight = cancelLogin;

let models: ManagedKimiCodeModelInfo[];
try {
models = await fetchOpenPlatformModels(platform, apiKey, fetch, controller.signal);
models = filterModelsByPrefix(models, platform);
} catch (error) {
if (controller.signal.aborted) return;
const msg = formatErrorMessage(error);
host.showError(`Failed to verify API key: ${msg}`);
if (
error instanceof OpenPlatformApiError &&
error.status === 401
) {
host.showStatus(
'Hint: If your API key was obtained from Kimi Code, please select "Kimi Code" instead.',
);
}
return;
} finally {
if (host.cancelInFlight === cancelLogin) {
host.cancelInFlight = undefined;
}
}

if (models.length === 0) {
host.showError('No models available for this platform.');
return;
}

const selection = await promptModelSelectionForOpenPlatform(host, models, platform);
if (selection === undefined) return;

const existingConfig = await host.harness.getConfig();
if (existingConfig.providers[platform.id] !== undefined) {
await host.harness.removeProvider(platform.id);
}

const config = await host.harness.getConfig();
applyOpenPlatformConfig(config as ManagedKimiConfigShape, {
platform,
models,
selectedModel: selection.model,
thinking: selection.thinking,
apiKey,
});

await host.harness.setConfig({
providers: config.providers,
models: config.models,
defaultModel: config.defaultModel,
defaultThinking: config.defaultThinking,
});

await host.authFlow.refreshConfigAfterLogin();
host.track('login', { provider: platform.id, method: 'api_key' });
host.showStatus(`Setup complete: ${platform.name} · ${selection.model.id}`);
}

export async function handleConnectCommand(host: SlashCommandHost, args: string): Promise<void> {
const resolution = resolveConnectCatalogRequest(args);
if (resolution.kind === 'error') {
host.showError(resolution.message);
return;
}
const { url, preferBuiltIn, allowBuiltInFallback } = resolution.request;

let catalog: Catalog | undefined;

if (preferBuiltIn) {
const builtIn = loadBuiltInCatalog(BUILT_IN_CATALOG_JSON);
if (builtIn !== undefined) {
host.showStatus('Loaded built-in catalog. Run /connect refresh for the latest.');
catalog = builtIn;
}
}

if (catalog === undefined) {
const controller = new AbortController();
const cancel = (): void => {
controller.abort();
};
host.cancelInFlight = cancel;

const spinner = host.showLoginProgressSpinner(`Fetching catalog from ${url}`);
try {
catalog = await fetchCatalog(url, controller.signal);
spinner.stop({ ok: true, label: 'Catalog loaded.' });
} catch (error) {
if (controller.signal.aborted) {
spinner.stop({ ok: false, label: 'Aborted.' });
} else {
const hint = error instanceof CatalogFetchError ? ` (HTTP ${error.status})` : '';
if (!allowBuiltInFallback) {
spinner.stop({ ok: false, label: 'Failed to load catalog.' });
host.showError(`Failed to fetch catalog${hint}: ${formatErrorMessage(error)}`);
} else {
const fallback = loadBuiltInCatalog(BUILT_IN_CATALOG_JSON);
if (fallback !== undefined) {
spinner.stop({ ok: true, label: 'Using built-in catalog (offline mode).' });
catalog = fallback;
} else {
spinner.stop({ ok: false, label: 'Failed to load catalog.' });
host.showError(`Failed to fetch catalog${hint}: ${formatErrorMessage(error)}`);
}
}
}
} finally {
if (host.cancelInFlight === cancel) host.cancelInFlight = undefined;
}
}

if (catalog === undefined) return;

const providerId = await promptCatalogProviderSelection(host, catalog);
if (providerId === undefined) return;
const entry = catalog[providerId];
if (entry === undefined) return;

const models = catalogProviderModels(entry);
if (models.length === 0) {
host.showError(`Provider "${providerId}" has no usable models in this catalog.`);
return;
}

const selection = await promptModelSelectionForCatalog(host, providerId, models);
if (selection === undefined) return;

const apiKey = await promptApiKey(host, entry.name ?? providerId);
if (apiKey === undefined) return;

const wire = inferWireType(entry);
if (wire === undefined) return;
const baseUrl = catalogBaseUrl(entry, wire);

const existingConfig = await host.harness.getConfig();
if (existingConfig.providers[providerId] !== undefined) {
await host.harness.removeProvider(providerId);
}

const config = await host.harness.getConfig();
applyCatalogProvider(config, {
providerId,
wire,
baseUrl,
apiKey,
models,
selectedModelId: selection.model.id,
thinking: selection.thinking,
});

await host.harness.setConfig({
providers: config.providers,
models: config.models,
defaultModel: config.defaultModel,
defaultThinking: config.defaultThinking,
});

await host.authFlow.refreshConfigAfterLogin();
host.track('connect', { provider: providerId, model: selection.model.id });
host.showStatus(`Connected: ${entry.name ?? providerId} · ${selection.model.id}`);
}

export async function handleLogoutCommand(host: SlashCommandHost): Promise<void> {
const oauthStatus = await host.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME);
const hasOAuthToken = oauthStatus.providers.some(
(p) => p.providerName === DEFAULT_OAUTH_PROVIDER_NAME && p.hasToken,
);
const config = await host.harness.getConfig();
const hasManagedRemnant =
hasOAuthToken || config.providers[DEFAULT_OAUTH_PROVIDER_NAME] !== undefined;
const apiKeyProviderIds = Object.keys(config.providers ?? {})
.filter((id) => id !== DEFAULT_OAUTH_PROVIDER_NAME)
.toSorted();

const options: ChoiceOption[] = [];
if (hasManagedRemnant) {
options.push({
value: DEFAULT_OAUTH_PROVIDER_NAME,
label: PRODUCT_NAME,
description: 'OAuth login',
});
}
for (const id of apiKeyProviderIds) {
const baseUrl = config.providers[id]?.baseUrl;
options.push({
value: id,
label: id,
description: typeof baseUrl === 'string' && baseUrl.length > 0 ? baseUrl : undefined,
});
}

if (options.length === 0) {
host.showStatus('Nothing to logout.');
return;
}

const currentModel = host.state.appState.model.trim();
const currentProvider = host.state.appState.availableModels[currentModel]?.provider;

const target = await promptLogoutProviderSelection(host, options, currentProvider);
if (target === undefined) return;

if (target === DEFAULT_OAUTH_PROVIDER_NAME) {
await host.harness.auth.logout(DEFAULT_OAUTH_PROVIDER_NAME);
} else {
await host.harness.removeProvider(target);
}

if (target === currentProvider) {
await host.authFlow.refreshConfigAfterLogout();
await host.authFlow.clearActiveSessionAfterLogout();
} else {
const updated = await host.harness.getConfig({ reload: true });
host.setAppState({
availableModels: updated.models ?? {},
availableProviders: updated.providers ?? {},
});
}

host.track('logout', { provider: target });
const label = target === DEFAULT_OAUTH_PROVIDER_NAME ? PRODUCT_NAME : target;
host.showStatus(`Logged out from ${label}.`);
}
Loading
Loading