Skip to content

feat(tui): add /provider command, custom registry import, and tabbed model selector#264

Open
sailist wants to merge 1 commit into
MoonshotAI:mainfrom
sailist:haozhe/provider-tui
Open

feat(tui): add /provider command, custom registry import, and tabbed model selector#264
sailist wants to merge 1 commit into
MoonshotAI:mainfrom
sailist:haozhe/provider-tui

Conversation

@sailist
Copy link
Copy Markdown
Collaborator

@sailist sailist commented Jun 1, 2026

Summary

This PR introduces a comprehensive provider/model management system for the TUI, including a new /provider slash command, a tabbed model selector, custom registry import support, and supporting UI dialogs. It also adds corresponding documentation updates and unit tests.


1. Provider & Model Management in TUI

Problem: Users previously had limited ability to manage LLM providers and select models directly from the TUI. Switching providers or importing custom registries required manual configuration file edits.

What was done:

  • Added a new /provider TUI slash command for managing providers interactively.
  • Implemented ProviderManagerDialog to list, add, edit, and remove providers.
  • Implemented TabbedModelSelector to let users browse and select models across providers in a tabbed interface.
  • Updated command dispatch and config wiring to integrate the new flows.

2. Custom Registry Import

Problem: There was no built-in way to import external provider registries (e.g., custom OpenAI-compatible endpoints) without manually editing configuration files.

What was done:

  • Added CustomRegistryImporter UI dialog for importing registries via URL or pasted JSON.
  • Added packages/oauth/src/custom-registry.ts module to parse, validate, and merge custom registry definitions.
  • Added comprehensive unit tests in packages/oauth/test/custom-registry.test.ts.

3. Documentation & Test Updates

Problem: New TUI commands and registry features lacked user-facing documentation, and existing tests needed updates for the changed command surface.

What was done:

  • Updated English and Chinese docs for provider configuration and slash-commands reference.
  • Updated existing TUI tests (choice-picker.test.ts, kimi-tui-message-flow.test.ts) to reflect new command additions.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 1, 2026

🦋 Changeset detected

Latest commit: ccb1060

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@moonshot-ai/kimi-code Minor
@moonshot-ai/kimi-code-oauth Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 1, 2026

pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@ccb1060
npx https://pkg.pr.new/@moonshot-ai/kimi-code@ccb1060

commit: ccb1060

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2d7290cd4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

baseUrl,
apiKey,
models,
selectedModelId: '', // no default yet; user picks in the model selector
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid writing an empty default model

When adding a known provider, this passes an empty selectedModelId into applyCatalogProvider, but that helper always sets config.defaultModel to ${providerId}/${selectedModelId}. If the user presses Esc in the following selector (which the comment says should leave the provider without a default selection), the config has already been saved with an invalid default such as anthropic/, so later config refreshes/startup cannot resolve the default model. Persist the provider/models without setting defaultModel, or clear it before setConfig when no model was selected.

Useful? React with 👍 / 👎.

Comment on lines +287 to +291
applyCustomRegistryProvider(
config as Parameters<typeof applyCustomRegistryProvider>[0],
entry as Parameters<typeof applyCustomRegistryProvider>[1],
source,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the provider before re-importing registry models

For an already-imported custom registry provider, applyCustomRegistryProvider deletes stale aliases only from this in-memory config, but host.harness.setConfig deep-merges patches, so aliases omitted from config['models'] are not removed from the existing TOML. Re-importing an api.json after a model is removed therefore leaves the old alias selectable, unlike the catalog path above which calls removeProvider before applying. Delete each existing provider id before applying its refreshed registry entry, or use a write path that can remove missing model keys.

Useful? React with 👍 / 👎.

type: entry.type,
baseUrl: entry.api,
apiKey: source.apiKey,
source,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist the registry source through config validation

Storing source here does not survive the actual setConfig path: provider patches are parsed by ProviderConfigSchema, which only keeps type, apiKey, baseUrl, defaultModel, oauth, env, and customHeaders. After import/reload, ProviderManagerComponent can no longer see cfg.source.kind === 'apiJson', so multiple providers from the same api.json are shown and deleted as unrelated standalone providers instead of one registry source. Add source to the managed provider schema/TOML handling or store the registry metadata in a supported field.

Useful? React with 👍 / 👎.

// only `adaptive_thinking = true`. Without this it would render as "unsupported"
// and switching to it would force thinking off.
if (caps.includes('thinking') || model.adaptiveThinking === true) return 'toggle';
if (caps.includes('thinking')) return 'toggle';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve adaptive-thinking aliases as toggleable

For aliases configured with adaptiveThinking: true but no explicit thinking capability, this now reports thinking as unsupported and effectiveThinking forces the selection to false. That regresses custom Anthropic endpoints that rely on adaptiveThinking to force adaptive thinking (the provider still forwards that alias field), so selecting the model from /model silently disables thinking instead of preserving/toggling it. Keep model.adaptiveThinking === true in the toggleable case.

Useful? React with 👍 / 👎.

this.opts = opts;
this.tabs = buildTabs(opts);

const preferredProvider = opts.initialTabId ?? opts.models[opts.currentValue]?.provider;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Select the tab for an explicit model alias

When /model <alias> names a model from a different provider than the current one, showModelPicker passes that alias as selectedValue, but the tabbed selector still opens the current provider's tab. That tab does not contain the requested alias, so pressing Enter confirms the current model instead of the alias the user typed; prefer opts.models[opts.selectedValue]?.provider before falling back to the current model provider.

Useful? React with 👍 / 👎.

Comment on lines +216 to +219
if (tokenValue.length === 0) {
this.hint = 'token-empty';
this.activeField = 'token';
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow tokenless custom registry imports

This dialog refuses to submit when the Bearer token field is empty, but fetchCustomRegistry explicitly supports an empty apiKey by omitting the Authorization header. As a result, users cannot import public/tokenless api.json registries through /provider even though the underlying importer can fetch them. Let an empty token submit as apiKey: '' instead of treating it as a validation error.

Useful? React with 👍 / 👎.

Comment on lines +47 to +48
name: 'provider',
aliases: ['providers'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve /connect compatibility or mark a major bump

Replacing the /connect registry entry with /provider leaves no compatibility alias for existing users and scripts; after this change /connect ... is no longer recognized as a slash command and falls through as a normal message to the model. The repo rules treat removed commands as breaking changes, so either keep /connect as an alias/forwarder to the provider catalog flow or ship this with an explicit major-version change.

Useful? React with 👍 / 👎.

@sailist sailist force-pushed the haozhe/provider-tui branch from b2d7290 to 79938e5 Compare June 1, 2026 02:54
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79938e511e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

aliases: ['providers'],
description: 'Manage AI providers (add / delete / refresh)',
priority: 95,
availability: 'always',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make /provider idle-only during generation

When the assistant is streaming or compacting, availability: 'always' lets /provider open and perform add/delete actions even though those callbacks mutate provider config and can call refreshConfigAfterLogin() or clearActiveSessionAfterLogout() without a busy-state guard. In that state, adding a provider may call session.setModel() through the refresh path, and deleting the active provider can close the active session mid-turn; the old provider-mutating /connect command was idle-only by default. Keep /provider idle-only or add an internal streaming/compacting guard before allowing mutations.

Useful? React with 👍 / 👎.

Comment on lines +261 to +262
if (typeof output === 'number' && Number.isInteger(output) && output > 0) {
return output;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve output limits separately from context

When an imported registry model has limit.output but no limit.context, this uses the output-token cap as maxContextSize; for example an 8192 output limit makes the model look like it only has an 8k context window. The parsed output limit is also never written as maxOutputSize on the alias, so runtime provider setup cannot forward it as the model's max-token budget. Leave the context at the default when only output is present and store output in maxOutputSize instead.

Useful? React with 👍 / 👎.

initialTabId: providerId,
onSelect: ({ alias, thinking }) => {
host.restoreEditor();
void setDefaultModel(host, alias, thinking);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle failures while saving the picked default

After adding a catalog provider, this callback fires setDefaultModel() without any catch, but that helper writes config and calls refreshConfigAfterLogin(), which can reject if the config write fails or if activating the newly selected provider fails (for example because the entered API key is invalid). Since this selection happens after the slash-command handler has already returned, the dispatcher’s try/catch does not see the rejection, leaving the user with no error message and potentially an unhandled promise rejection; mirror performModelSwitch and surface the failure through host.showError.

Useful? React with 👍 / 👎.

…model selector

- add "/provider" slash command for managing AI providers with CRUD UI\n- add custom registry import via api.json URL and Bearer token\n- introduce tabbed model selector grouped by provider\n- add fetchCustomRegistry and applyCustomRegistryProvider in oauth package\n- replace deprecated "/connect" command with unified "/provider" flow\n- update provider and slash-command documentation
@sailist sailist force-pushed the haozhe/provider-tui branch from 79938e5 to ccb1060 Compare June 1, 2026 03:20
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccb10605c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

aliases: [],
description: 'Switch LLM model',
priority: 100,
availability: 'always',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep /model blocked while compacting

When the app is compacting (isCompacting true while streamingPhase can still be idle), marking /model as always lets the picker open and then performModelSwitch proceeds because its only busy guard checks streamingPhase !== 'idle'; selecting a model can therefore call session.setModel/setThinking and persist defaults during compaction. Keep this command idle-only or add an isCompacting guard before applying the selection.

Useful? React with 👍 / 👎.

@sailist
Copy link
Copy Markdown
Collaborator Author

sailist commented Jun 1, 2026

@codex

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

'Run /login to sign in to Kimi, or /connect to add another provider from a model catalog.',

P3 Badge Replace stale /connect guidance after removing the command

This empty-model notice still tells users to run /connect, but the same change removes /connect from the command registry and dispatch path, so in this exact no-model state the suggested command is no longer executable and will be treated as normal input. Point users at /provider instead so they can actually add a provider from the TUI.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

let config = await host.harness.getConfig();
for (const entry of Object.values(entries)) {
if (config.providers[entry.id] !== undefined) {
config = await host.harness.removeProvider(entry.id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve earlier imports when replacing registry providers

When re-importing a custom registry that contains multiple providers already present in the config, this call reloads the config from disk on each iteration. Any provider applied earlier in the loop has not been persisted yet, so a later removeProvider() overwrites config with a snapshot that does not include that earlier in-memory addition; after the final setConfig, only the providers applied after the last removal survive. Remove all conflicting providers before applying entries, or delete stale providers/models from the in-memory config instead of replacing config mid-loop.

Useful? React with 👍 / 👎.

description: 'Manage AI providers (add / delete / refresh)',
priority: 95,
availability: 'always',
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep provider management idle-only during active turns

Marking /provider as always lets users open it while a response is streaming or compaction is running, but the add/delete flows mutate the active runtime: picking a default after adding a provider goes through setDefaultModel()refreshConfigAfterLogin()activateModelAfterLogin(), which calls session.setModel() without the non-idle guard used by /model, and deleting the active provider can close the session. This can change or tear down the current session while a turn is still in flight, so provider mutations should stay idle-only or guard these actions explicitly.

Useful? React with 👍 / 👎.

aliases: [],
description: 'Switch LLM model',
priority: 100,
availability: 'always',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Keep /model idle-only until streaming switches work

With this availability change, /model is no longer blocked during streaming, so the user can spend time in the selector, but selecting a model still immediately hits performModelSwitch()'s streamingPhase !== 'idle' check and only shows Cannot switch models while streaming. Either keep the command idle-only or remove the later guard so the advertised always-available workflow can actually complete.

Useful? React with 👍 / 👎.

models: config.models,
});

await host.authFlow.refreshConfigAfterLogin();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh providers without reactivating the saved default

At this point the provider has only been added and the user has not picked the new default yet, but refreshConfigAfterLogin() also calls activateModelAfterLogin() for whatever config.defaultModel currently says. If the current session is using a runtime-selected model that differs from the saved default, simply adding a provider can silently switch the session back before the selector appears (and canceling the selector leaves it there); use a model/provider snapshot refresh here and only activate after setDefaultModel().

Useful? React with 👍 / 👎.

const spinner = host.showLoginProgressSpinner(`Fetching catalog from ${DEFAULT_CATALOG_URL}`);
let catalog: Catalog | undefined;
try {
catalog = await fetchCatalog(DEFAULT_CATALOG_URL, controller.signal);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the built-in catalog fallback for provider add

The new known-provider path always fetches DEFAULT_CATALOG_URL and returns on failure, while the removed /connect path first used the bundled catalog and could fall back to it when the network was unavailable. Since /connect is no longer reachable, users behind a proxy or offline can no longer add catalog providers even though the built-in catalog is still shipped; load BUILT_IN_CATALOG_JSON as the default/fallback here or keep an equivalent command path.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant