diff --git a/.changeset/provider-replaces-connect-hints.md b/.changeset/provider-replaces-connect-hints.md new file mode 100644 index 0000000000..5b60062052 --- /dev/null +++ b/.changeset/provider-replaces-connect-hints.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Point users to `/provider` instead of the removed `/connect` command in the welcome screen and the no-models-configured hint. diff --git a/apps/kimi-code/src/tui/commands/auth.ts b/apps/kimi-code/src/tui/commands/auth.ts index bb2622f075..8064c089b6 100644 --- a/apps/kimi-code/src/tui/commands/auth.ts +++ b/apps/kimi-code/src/tui/commands/auth.ts @@ -8,36 +8,22 @@ import { 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 { log } 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 +// Auth: login / logout // --------------------------------------------------------------------------- export async function handleLoginCommand(host: SlashCommandHost): Promise { @@ -188,110 +174,6 @@ async function handleOpenPlatformLogin( host.showStatus(`Setup complete: ${platform.name} · ${selection.model.id}`); } -export async function handleConnectCommand(host: SlashCommandHost, args: string): Promise { - 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 { const oauthStatus = await host.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME); const hasOAuthToken = oauthStatus.providers.some( diff --git a/apps/kimi-code/src/tui/commands/config.ts b/apps/kimi-code/src/tui/commands/config.ts index c70bc8a2c7..b602f800ec 100644 --- a/apps/kimi-code/src/tui/commands/config.ts +++ b/apps/kimi-code/src/tui/commands/config.ts @@ -251,7 +251,7 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string = if (entries.length === 0) { host.showNotice( 'No models configured', - 'Run /login to sign in to Kimi, or /connect to add another provider from a model catalog.', + 'Run /login to sign in to Kimi, or /provider to add another provider from a model catalog.', ); return; } diff --git a/apps/kimi-code/src/tui/commands/dispatch.ts b/apps/kimi-code/src/tui/commands/dispatch.ts index 3737178d7a..1387a62ffa 100644 --- a/apps/kimi-code/src/tui/commands/dispatch.ts +++ b/apps/kimi-code/src/tui/commands/dispatch.ts @@ -50,7 +50,6 @@ import { // --------------------------------------------------------------------------- export { - handleConnectCommand, handleLoginCommand, handleLogoutCommand, } from './auth'; diff --git a/apps/kimi-code/src/tui/commands/index.ts b/apps/kimi-code/src/tui/commands/index.ts index 60178b2653..8c9f1730fb 100644 --- a/apps/kimi-code/src/tui/commands/index.ts +++ b/apps/kimi-code/src/tui/commands/index.ts @@ -7,7 +7,6 @@ export * from './types'; export { dispatchInput, type SlashCommandHost } from './dispatch'; export { - handleConnectCommand, handleLoginCommand, handleLogoutCommand, } from './auth'; diff --git a/apps/kimi-code/src/tui/components/chrome/welcome.ts b/apps/kimi-code/src/tui/components/chrome/welcome.ts index 4228c56840..0f8a0b05ef 100644 --- a/apps/kimi-code/src/tui/components/chrome/welcome.ts +++ b/apps/kimi-code/src/tui/components/chrome/welcome.ts @@ -42,7 +42,7 @@ export class WelcomeComponent implements Component { const dim = chalk.hex(this.colors.textDim); const labelStyle = chalk.bold.hex(this.colors.textDim); const rightRow1 = truncateToWidth( - dim(isLoggedOut ? 'Run /login or /connect to get started.' : 'Send /help for help information.'), + dim(isLoggedOut ? 'Run /login or /provider to get started.' : 'Send /help for help information.'), textWidth, '…', ); @@ -57,7 +57,7 @@ export class WelcomeComponent implements Component { const activeModel = this.state.availableModels[this.state.model]; const modelValue = isLoggedOut - ? chalk.hex(this.colors.warning)('not set, run /login or /connect') + ? chalk.hex(this.colors.warning)('not set, run /login or /provider') : (activeModel?.displayName ?? activeModel?.model ?? this.state.model); const infoLines = [ diff --git a/apps/kimi-code/src/tui/utils/connect-catalog.ts b/apps/kimi-code/src/tui/utils/connect-catalog.ts deleted file mode 100644 index dfd86bda52..0000000000 --- a/apps/kimi-code/src/tui/utils/connect-catalog.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DEFAULT_CATALOG_URL } from '@moonshot-ai/kimi-code-sdk'; - -const BARE_HTTP_URL_RE = /^https?:\/\/\S+$/; - -export interface ConnectCatalogRequest { - readonly url: string; - readonly preferBuiltIn: boolean; - readonly allowBuiltInFallback: boolean; -} - -export type ConnectCatalogResolution = - | { readonly kind: 'ok'; readonly request: ConnectCatalogRequest } - | { readonly kind: 'error'; readonly message: string }; - -export function resolveConnectCatalogRequest(args: string): ConnectCatalogResolution { - const trimmed = args.trim(); - - if (trimmed === '') { - return { - kind: 'ok', - request: { - url: DEFAULT_CATALOG_URL, - preferBuiltIn: true, - allowBuiltInFallback: true, - }, - }; - } - - const tokens = trimmed.split(/\s+/).filter(Boolean); - let explicitUrl: string | undefined; - let refreshRequested = false; - - for (const token of tokens) { - if (token.toLowerCase() === 'refresh') { - refreshRequested = true; - continue; - } - - if (BARE_HTTP_URL_RE.test(token)) { - if (explicitUrl !== undefined) { - return { - kind: 'error', - message: `Only one catalog URL can be provided. Got "${explicitUrl}" and "${token}".`, - }; - } - explicitUrl = token; - continue; - } - - if (token.startsWith('--')) { - return { - kind: 'error', - message: `Unexpected flag "${token}". Use /connect [url] [refresh] instead.`, - }; - } - - return { - kind: 'error', - message: `Unknown argument "${token}". Usage: /connect [url] [refresh]`, - }; - } - - if (explicitUrl !== undefined) { - return { - kind: 'ok', - request: { - url: explicitUrl, - preferBuiltIn: false, - allowBuiltInFallback: false, - }, - }; - } - - return { - kind: 'ok', - request: { - url: DEFAULT_CATALOG_URL, - preferBuiltIn: !refreshRequested, - allowBuiltInFallback: true, - }, - }; -} diff --git a/apps/kimi-code/test/tui/utils/connect-catalog.test.ts b/apps/kimi-code/test/tui/utils/connect-catalog.test.ts deleted file mode 100644 index 98ece0d296..0000000000 --- a/apps/kimi-code/test/tui/utils/connect-catalog.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { DEFAULT_CATALOG_URL, loadBuiltInCatalog } from '@moonshot-ai/kimi-code-sdk'; -import { describe, expect, it } from 'vitest'; - -import { BUILT_IN_CATALOG_JSON } from '#/built-in-catalog'; -import { resolveConnectCatalogRequest } from '#/tui/utils/connect-catalog'; - -import { builtInCatalogDefine } from '../../../scripts/built-in-catalog.mjs'; - -describe('resolveConnectCatalogRequest', () => { - it('prefers the built-in catalog by default and keeps online fetch as fallback', () => { - expect(resolveConnectCatalogRequest('')).toEqual({ - kind: 'ok', - request: { - url: DEFAULT_CATALOG_URL, - preferBuiltIn: true, - allowBuiltInFallback: true, - }, - }); - }); - - it('forces an online fetch when refresh is requested', () => { - expect(resolveConnectCatalogRequest('refresh')).toEqual({ - kind: 'ok', - request: { - url: DEFAULT_CATALOG_URL, - preferBuiltIn: false, - allowBuiltInFallback: true, - }, - }); - expect(resolveConnectCatalogRequest(' refresh ')).toEqual({ - kind: 'ok', - request: { - url: DEFAULT_CATALOG_URL, - preferBuiltIn: false, - allowBuiltInFallback: true, - }, - }); - }); - - it('treats explicit catalog URLs as authoritative and ignores refresh on them', () => { - expect(resolveConnectCatalogRequest('https://internal.example/catalog.json')).toEqual({ - kind: 'ok', - request: { - url: 'https://internal.example/catalog.json', - preferBuiltIn: false, - allowBuiltInFallback: false, - }, - }); - expect( - resolveConnectCatalogRequest('refresh https://internal.example/catalog.json'), - ).toEqual({ - kind: 'ok', - request: { - url: 'https://internal.example/catalog.json', - preferBuiltIn: false, - allowBuiltInFallback: false, - }, - }); - expect( - resolveConnectCatalogRequest('https://internal.example/catalog.json refresh'), - ).toEqual({ - kind: 'ok', - request: { - url: 'https://internal.example/catalog.json', - preferBuiltIn: false, - allowBuiltInFallback: false, - }, - }); - }); - - it('rejects unsupported flags', () => { - const flagMessage = (flag: string) => - `Unexpected flag "${flag}". Use /connect [url] [refresh] instead.`; - expect(resolveConnectCatalogRequest('--refresh')).toEqual({ - kind: 'error', - message: flagMessage('--refresh'), - }); - expect(resolveConnectCatalogRequest('--url=https://internal.example/catalog.json')).toEqual({ - kind: 'error', - message: flagMessage('--url=https://internal.example/catalog.json'), - }); - expect(resolveConnectCatalogRequest('--url https://internal.example/catalog.json')).toEqual({ - kind: 'error', - message: flagMessage('--url'), - }); - }); - - it('rejects non-URL bare tokens', () => { - expect(resolveConnectCatalogRequest('ignored text')).toEqual({ - kind: 'error', - message: 'Unknown argument "ignored". Usage: /connect [url] [refresh]', - }); - }); - - it('rejects multiple URLs', () => { - expect( - resolveConnectCatalogRequest('https://a.com/x.json https://b.com/y.json'), - ).toEqual({ - kind: 'error', - message: 'Only one catalog URL can be provided. Got "https://a.com/x.json" and "https://b.com/y.json".', - }); - }); -}); - -describe('built-in connect catalog injection', () => { - it('keeps the source placeholder empty so generated catalog data is not committed', () => { - expect(BUILT_IN_CATALOG_JSON).toBeUndefined(); - expect(loadBuiltInCatalog(BUILT_IN_CATALOG_JSON)).toBeUndefined(); - }); - - it('embeds a generated catalog file through the tsdown define value', async () => { - const catalog = { - openai: { - id: 'openai', - npm: '@ai-sdk/openai', - models: { - 'gpt-test': { - id: 'gpt-test', - limit: { context: 1000, output: 100 }, - modalities: { input: ['text'], output: ['text'] }, - }, - }, - }, - }; - const dir = await mkdtemp(join(tmpdir(), 'kimi-built-in-catalog-')); - try { - const file = join(dir, 'catalog.json'); - const text = JSON.stringify(catalog); - await writeFile(file, text, 'utf-8'); - - const defineValue = builtInCatalogDefine({ KIMI_CODE_BUILT_IN_CATALOG_FILE: file }); - expect(JSON.parse(defineValue)).toBe(text); - expect(loadBuiltInCatalog(JSON.parse(defineValue))).toEqual(catalog); - } finally { - await rm(dir, { recursive: true, force: true }); - } - }); -});