From d3e5bd9ac2fc9a3ff5f22f607838d11097142c78 Mon Sep 17 00:00:00 2001 From: PaiduiXiaowangzi Date: Sat, 15 Aug 2026 17:13:51 +0800 Subject: [PATCH 1/2] fix(tui): add provider refresh action --- .changeset/refresh-provider-manager.md | 5 +++ apps/kimi-code/src/tui/commands/provider.ts | 25 +++++++++++++++ .../components/dialogs/provider-manager.ts | 11 +++++-- .../src/tui/controllers/auth-flow.ts | 4 +-- .../dialogs/provider-manager.test.ts | 19 ++++++++++++ .../test/tui/kimi-tui-startup.test.ts | 31 +++++++++++++++++++ 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 .changeset/refresh-provider-manager.md diff --git a/.changeset/refresh-provider-manager.md b/.changeset/refresh-provider-manager.md new file mode 100644 index 0000000000..09ab6c2020 --- /dev/null +++ b/.changeset/refresh-provider-manager.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add an explicit refresh action to the `/provider` manager. Press `R` to refresh provider models. diff --git a/apps/kimi-code/src/tui/commands/provider.ts b/apps/kimi-code/src/tui/commands/provider.ts index 61ec07b911..214dd52bc9 100644 --- a/apps/kimi-code/src/tui/commands/provider.ts +++ b/apps/kimi-code/src/tui/commands/provider.ts @@ -60,6 +60,11 @@ function buildProviderManagerOptions(host: SlashCommandHost): ProviderManagerOpt host.showError(`Add provider failed: ${formatErrorMessage(error)}`); }); }, + onRefresh: () => { + void handleProviderManagerRefresh(host).catch((error: unknown) => { + host.showError(`Refresh providers failed: ${formatErrorMessage(error)}`); + }); + }, onDeleteSource: (providerIds) => { void handleProviderManagerDeleteSource(host, providerIds).catch((error: unknown) => { host.showError(`Remove provider failed: ${formatErrorMessage(error)}`); @@ -71,6 +76,26 @@ function buildProviderManagerOptions(host: SlashCommandHost): ProviderManagerOpt }; } +async function handleProviderManagerRefresh(host: SlashCommandHost): Promise { + const spinner = host.showProgressSpinner('Refreshing provider models...'); + try { + const result = await host.authFlow.refreshProviderModels(); + const ok = result.failed.length === 0; + spinner.stop({ + ok, + label: ok ? 'Provider refresh finished.' : 'Provider refresh finished with warnings.', + }); + for (const failure of result.failed) { + host.showStatus(`Skipped refreshing ${failure.provider}: ${failure.reason}`, 'warning'); + } + } catch (error) { + spinner.stop({ ok: false, label: 'Provider refresh failed.' }); + throw error; + } finally { + reopenProviderManager(host); + } +} + async function handleProviderManagerDeleteSource( host: SlashCommandHost, providerIds: readonly string[], diff --git a/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts b/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts index 7ae0da03d2..3403dc4555 100644 --- a/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts +++ b/apps/kimi-code/src/tui/components/dialogs/provider-manager.ts @@ -14,6 +14,7 @@ * - ↑ / ↓ move highlight * - ← / → · PgUp/PgDn page * - Enter on `[ Add New Platform ]` → `onAdd()` + * - R refresh provider models via `onRefresh()` * - D delete with inline `[y/N]` confirmation * on a source row → `onDeleteSource(providerIds)` * on `[ Add New Platform ]` → ignored @@ -61,6 +62,7 @@ export interface ProviderManagerOptions { /** Provider id of the currently active model. */ readonly activeProviderId?: string; readonly onAdd: () => void; + readonly onRefresh: () => void; /** Delete all providers under a source (Open Platform / custom-registry * fetch / standalone). Passed the full provider-id list so the host * doesn't have to re-derive the source grouping. */ @@ -91,7 +93,7 @@ type Row = SourceRow | AddRow; const ADD_ROW_LABEL = '[ Add New Platform ]'; const PAGE_SIZE = 8; -const HEADER_HINT = '↑↓ navigate · D delete · Esc cancel'; +const HEADER_HINT = '↑↓ navigate · R refresh · D delete · Esc cancel'; // Narrows a `ProviderConfig` blob to a `CustomRegistrySource` payload. // Mirrors `readCustomRegistrySource` in `kimi-tui.ts`. We can't import @@ -312,8 +314,13 @@ export class ProviderManagerComponent extends Container implements Focusable { return; } - // Delete the highlighted provider with the D key. const ch = printableChar(data); + if (ch === 'r' || ch === 'R') { + this.opts.onRefresh(); + return; + } + + // Delete the highlighted provider with the D key. if (ch === 'd' || ch === 'D') { this.armDeleteConfirm(); } diff --git a/apps/kimi-code/src/tui/controllers/auth-flow.ts b/apps/kimi-code/src/tui/controllers/auth-flow.ts index 67fac913c2..b1718305f2 100644 --- a/apps/kimi-code/src/tui/controllers/auth-flow.ts +++ b/apps/kimi-code/src/tui/controllers/auth-flow.ts @@ -210,9 +210,7 @@ export class AuthFlowController { private async refreshProviderModelsWithScope(scope: RefreshProviderScope): Promise { const result = await refreshAllProviderModels(this.buildRefreshHost(), { scope }); - if (result.changed.length > 0) { - await this.refreshAvailableModels(); - } + await this.refreshAvailableModels(); return result; } diff --git a/apps/kimi-code/test/tui/components/dialogs/provider-manager.test.ts b/apps/kimi-code/test/tui/components/dialogs/provider-manager.test.ts index 6596cf705b..fa63f992a7 100644 --- a/apps/kimi-code/test/tui/components/dialogs/provider-manager.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/provider-manager.test.ts @@ -1,3 +1,8 @@ +/** + * Scenario: Provider Manager rendering and keyboard actions. + * Responsibilities: expose stable provider actions and dispatch public callbacks. + * Wiring: real component with callback spies; run with this file through Vitest. + */ import type { ProviderConfig } from '@moonshot-ai/kimi-code-sdk'; import chalk from 'chalk'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; @@ -25,6 +30,7 @@ function makeComponent(overrides: Partial = {}): Provide return new ProviderManagerComponent({ providers: {} as Record, onAdd: vi.fn(), + onRefresh: vi.fn(), onDeleteSource: vi.fn(), onClose: vi.fn(), ...overrides, @@ -123,6 +129,19 @@ describe('ProviderManagerComponent', () => { expect(onDeleteSource).toHaveBeenCalledWith(['acme']); }); + it('dispatches refresh when R arrives through the Kitty keyboard protocol', () => { + const onRefresh = vi.fn(); + const component = makeComponent({ onRefresh }); + + component.handleInput(`${ESC}[114u`); + + expect(onRefresh).toHaveBeenCalledOnce(); + }); + + it('advertises the R refresh shortcut in the header hint', () => { + expect(rendered(makeComponent())).toContain('R refresh'); + }); + it('closes on Esc', () => { const onClose = vi.fn(); const component = makeComponent({ diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts index 1d3132a352..97008fee24 100644 --- a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -36,6 +36,7 @@ const copyTextToClipboardMock = vi.mocked(copyTextToClipboard); interface StartupDriver { state: TUIState; + readonly authFlow: KimiTUI['authFlow']; init(): Promise; handleLoginCommand(): Promise; handleLogoutCommand(): Promise; @@ -1511,6 +1512,36 @@ describe('KimiTUI startup', () => { expect(showStatus).toHaveBeenCalledWith("New Models · +2 models."); }); + it('reloads provider state when a refresh reports no model changes', async () => { + const getConfig = vi + .fn() + .mockResolvedValueOnce({ providers: {}, models: {} }) + .mockResolvedValue({ + providers: { + acme: { + type: 'openai', + baseUrl: 'https://api.example.test/v1', + apiKey: 'YOUR_API_KEY', + }, + }, + models: { + 'acme/example-model': { + provider: 'acme', + model: 'example-model', + maxContextSize: 4096, + capabilities: [], + }, + }, + }); + const harness = makeHarness(makeSession(), { getConfig }); + const driver = makeDriver(harness, makeStartupInput()); + + await driver.authFlow.refreshProviderModels(); + + expect(driver.state.appState.availableProviders).toHaveProperty('acme'); + expect(driver.state.appState.availableModels).toHaveProperty('acme/example-model'); + }); + it("stages provider-refresh removals and persists one atomic write on atomic-capable harnesses", async () => { const registryUrl = "https://registry.example.test/v1/models/api.json"; const source = { kind: "apiJson", url: registryUrl, apiKey: "sk-test-token" }; From cc186229c321c8a2584417fe90c556e0fa95f830 Mon Sep 17 00:00:00 2001 From: PaiduiXiaowangzi Date: Sun, 16 Aug 2026 00:20:42 +0800 Subject: [PATCH 2/2] fix(tui): keep dismissed provider manager closed --- apps/kimi-code/src/tui/commands/provider.ts | 5 +- .../dialogs/provider-manager.test.ts | 56 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/apps/kimi-code/src/tui/commands/provider.ts b/apps/kimi-code/src/tui/commands/provider.ts index 214dd52bc9..637bfe713b 100644 --- a/apps/kimi-code/src/tui/commands/provider.ts +++ b/apps/kimi-code/src/tui/commands/provider.ts @@ -77,6 +77,7 @@ function buildProviderManagerOptions(host: SlashCommandHost): ProviderManagerOpt } async function handleProviderManagerRefresh(host: SlashCommandHost): Promise { + const managerAtRefreshStart = host.state.editorContainer.children[0]; const spinner = host.showProgressSpinner('Refreshing provider models...'); try { const result = await host.authFlow.refreshProviderModels(); @@ -92,7 +93,9 @@ async function handleProviderManagerRefresh(host: SlashCommandHost): Promise { expect(rendered(makeComponent())).toContain('R refresh'); }); + it('keeps the editor active when provider refresh finishes after the manager is dismissed', async () => { + const editor = {}; + let focused: unknown = editor; + const editorChildren: unknown[] = [editor]; + let finishRefresh!: () => void; + const refreshProviderModels = vi.fn( + () => + new Promise<{ changed: []; unchanged: []; failed: [] }>((resolve) => { + finishRefresh = () => { + resolve({ changed: [], unchanged: [], failed: [] }); + }; + }), + ); + const host = { + state: { + appState: { + availableModels: {}, + availableProviders: {}, + model: '', + }, + editorContainer: { children: editorChildren }, + }, + authFlow: { refreshProviderModels }, + mountEditorReplacement: (component: unknown) => { + editorChildren.splice(0, editorChildren.length, component); + focused = component; + }, + restoreEditor: () => { + editorChildren.splice(0, editorChildren.length, editor); + focused = editor; + }, + showError: vi.fn(), + showProgressSpinner: () => ({ stop: vi.fn() }), + showStatus: vi.fn(), + } as unknown as Parameters[0]; + await handleProviderCommand(host); + const manager = focused as ProviderManagerComponent; + + manager.handleInput('R'); + expect(refreshProviderModels).toHaveBeenCalledOnce(); + manager.handleInput(ESC); + expect(focused).toBe(editor); + + finishRefresh(); + await Promise.resolve(); + + expect(focused).toBe(editor); + }); + it('closes on Esc', () => { const onClose = vi.fn(); const component = makeComponent({