Skip to content
Open
Show file tree
Hide file tree
Changes from all 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/tui-status-line-provider-slot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add a `provider` slot to the TUI footer status line, so the status bar can show which provider serves the current model. List it in `[status_line].items` to enable it, and it is also available as `provider` in the `[status_line].command` JSON payload.
21 changes: 21 additions & 0 deletions apps/kimi-code/src/tui/components/chrome/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { isRainbowDancing, renderDanceFooterModel } from '#/tui/easter-eggs/danc
import { currentTheme } from '#/tui/theme';
import type { ColorPalette } from '#/tui/theme/colors';
import type { AppState } from '#/tui/types';
import { providerDisplayName } from '#/tui/utils/provider-label';
import {
StatusLineCommandRunner,
type StatusLinePayload,
Expand Down Expand Up @@ -147,6 +148,19 @@ function modelDisplayName(state: AppState): string {
return effective?.displayName ?? effective?.model ?? state.model;
}

/**
* Provider serving the current model, as a display label. Empty when the
* active alias is not in the catalog, which drops the slot rather than
* echoing the raw alias — unlike the model name, an alias is no answer to
* "which provider".
*/
function modelProviderName(state: AppState): string {
const model = state.availableModels[state.model];
const effective = model === undefined ? undefined : effectiveModelAlias(model);
const provider = effective?.provider;
return provider === undefined || provider.length === 0 ? '' : providerDisplayName(provider);
}

function shortenCwd(path: string): string {
if (!path) return path;
const home = process.env['HOME'] ?? '';
Expand Down Expand Up @@ -362,6 +376,7 @@ export class FooterComponent implements Component {
const slots: Record<string, string[]> = {
mode: [],
goal: [],
provider: [],
model: [],
tasks: [],
cwd: [],
Expand All @@ -385,6 +400,11 @@ export class FooterComponent implements Component {
const goalBadge = formatGoalBadge(state.goal, colors, this.goalWallClockMs(state.goal));
if (goalBadge !== null) slots['goal'] = [goalBadge];

// Opt-in slot: the provider behind the current model, so a footer that
// lists several look-alike model names still says who serves them.
const provider = modelProviderName(state);
if (provider) slots['provider'] = [chalk.hex(colors.textDim)(provider)];

const model = modelDisplayName(state);
if (model) {
const effort = state.thinkingEffort;
Expand Down Expand Up @@ -439,6 +459,7 @@ export class FooterComponent implements Component {
const state = this.state;
return {
model: modelDisplayName(state),
provider: modelProviderName(state),
cwd: state.workDir,
gitBranch: this.gitCache.getStatus()?.branch ?? null,
permissionMode: state.permissionMode,
Expand Down
8 changes: 1 addition & 7 deletions apps/kimi-code/src/tui/components/dialogs/model-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
type Focusable,
} from '@moonshot-ai/pi-tui';

import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '#/constant/app';
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';
import { providerDisplayName } from '#/tui/utils/provider-label';
import { SearchableList } from '#/tui/utils/searchable-list';

import type { ChoiceOption } from './choice-picker';
Expand Down Expand Up @@ -42,12 +42,6 @@ export function modelDisplayName(alias: string, model: ModelAlias | undefined):
return effective?.displayName ?? effective?.model ?? alias;
}

export function providerDisplayName(provider: string): string {
if (provider === DEFAULT_OAUTH_PROVIDER_NAME) return PRODUCT_NAME;
if (provider.startsWith('managed:')) return provider.slice('managed:'.length);
return provider;
}

export function createModelChoiceOptions(
models: Record<string, ModelAlias>,
): readonly ChoiceOption[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import {
} from '@moonshot-ai/pi-tui';

import { currentTheme } from '#/tui/theme';
import { providerDisplayName } from '#/tui/utils/provider-label';
import { renderTabStrip } from '#/tui/utils/tab-strip';

import {
ModelSelectorComponent,
providerDisplayName,
type ModelSelection,
type ModelSelectorOptions,
} from './model-selector';
Expand Down
11 changes: 10 additions & 1 deletion apps/kimi-code/src/tui/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@ export const UpgradePreferencesSchema = z.object({
autoInstall: z.boolean(),
});

export const STATUS_LINE_ITEMS = ['mode', 'goal', 'model', 'tasks', 'cwd', 'git', 'tips'] as const;
export const STATUS_LINE_ITEMS = [
'mode',
'goal',
'provider',
'model',
'tasks',
'cwd',
'git',
'tips',
] as const;
export type StatusLineItem = (typeof STATUS_LINE_ITEMS)[number];

export const StatusLineFileConfigSchema = z.object({
Expand Down
16 changes: 16 additions & 0 deletions apps/kimi-code/src/tui/utils/provider-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Readable label for a configured provider id.
*
* Config stores provider ids (`managed:kimi-code`, `managed:<vendor>`, or a
* user-chosen name); every surface that shows one to the user — the model
* dialogs and the footer status line — renders it through here so the
* `managed:` plumbing never leaks into the UI.
*/

import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '#/constant/app';

export function providerDisplayName(provider: string): string {
if (provider === DEFAULT_OAUTH_PROVIDER_NAME) return PRODUCT_NAME;
if (provider.startsWith('managed:')) return provider.slice('managed:'.length);
return provider;
}
2 changes: 2 additions & 0 deletions apps/kimi-code/src/tui/utils/status-line-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const STATUS_LINE_MAX_CAPTURE_BYTES = 65_536;

export interface StatusLinePayload {
model: string;
/** Provider serving the current model; empty when the alias is unknown. */
provider: string;
cwd: string;
gitBranch: string | null;
permissionMode: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import type { ModelAlias } from '@moonshot-ai/kimi-code-sdk';
import { describe, expect, it } from 'vitest';

import { FooterComponent } from '#/tui/components/chrome/footer';
Expand Down Expand Up @@ -41,8 +42,15 @@ const baseState: AppState = {
mcpServersSummary: null,
};

const catalogModel: ModelAlias = {
provider: 'moonshot',
model: 'kimi-k2-0905',
maxContextSize: 262_144,
};

const payload: StatusLinePayload = {
model: 'kimi-k2',
provider: 'moonshot',
cwd: '/tmp/project',
gitBranch: 'main',
permissionMode: 'manual',
Expand Down Expand Up @@ -122,6 +130,46 @@ describe('FooterComponent status_line items', () => {
expect(tipsLast.indexOf('kimi-k2')).toBeLessThan(tipsLast.indexOf(tipsOnly));
});

it('renders the provider ahead of the model when the slot is configured', () => {
const state: AppState = {
...baseState,
availableModels: { 'kimi-k2': catalogModel },
statusLine: { items: ['provider', 'model'], command: null },
};

const line1 = plain(new FooterComponent(state).render(120)[0]!);
const providerAt = line1.indexOf('moonshot');
const modelAt = line1.indexOf('kimi-k2-0905');
expect(providerAt).toBeGreaterThanOrEqual(0);
expect(modelAt).toBeGreaterThan(providerAt);
});

it('keeps the provider out of the default layout', () => {
const state: AppState = { ...baseState, availableModels: { 'kimi-k2': catalogModel } };

expect(plain(new FooterComponent(state).render(200)[0]!)).not.toContain('moonshot');
});

it('strips the managed: prefix from the provider label', () => {
const state: AppState = {
...baseState,
availableModels: { 'kimi-k2': { ...catalogModel, provider: 'managed:openrouter' } },
statusLine: { items: ['provider'], command: null },
};

const line1 = plain(new FooterComponent(state).render(120)[0]!).trim();
expect(line1).toBe('openrouter');
});

it('drops the provider slot when the active model is not in the catalog', () => {
const state: AppState = {
...baseState,
statusLine: { items: ['provider', 'model'], command: null },
};

expect(plain(new FooterComponent(state).render(120)[0]!).trim()).toBe('kimi-k2');
});

it('renders nothing on line 1 for an empty items list', () => {
const state: AppState = {
...baseState,
Expand All @@ -140,6 +188,7 @@ describe('runStatusLineCommand', () => {
expect(line).not.toBeNull();
const parsed = JSON.parse(line!);
expect(parsed.model).toBe('kimi-k2');
expect(parsed.provider).toBe('moonshot');
expect(parsed.gitBranch).toBe('main');
expect(parsed.cwd).toBe('/tmp/project');
});
Expand Down
10 changes: 7 additions & 3 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,12 @@ Alongside `config.toml`, the CLI keeps terminal-UI and client preferences in a c
| `[notifications].enabled` | `boolean` | `true` | Whether desktop notifications are sent |
| `[notifications].notification_condition` | `string` | `unfocused` | When to notify: `unfocused` (only when the terminal is not focused) or `always` |
| `[upgrade].auto_install` | `boolean` | `true` | Whether new versions are installed automatically |
| `[status_line].items` | `string[]` | `[]` | Built-in slots to show on the first footer line and their order: `mode`, `goal`, `model`, `tasks`, `cwd`, `git`, `tips`. Unset keeps the default layout; unknown ids are skipped with a warning |
| `[status_line].command` | `string` | `""` | Custom status line command. Its first stdout line replaces the first footer line, with a JSON snapshot (model, cwd, git branch, permission mode, plan mode, context usage, session id, version) passed on stdin. Runs are capped at 300ms and throttled to once per second; failures fall back to the built-in layout |
| `[status_line].items` | `string[]` | `[]` | Built-in slots to show on the first footer line and their order: `mode`, `goal`, `provider`, `model`, `tasks`, `cwd`, `git`, `tips`. Unset keeps the default layout, which does not include `provider`; unknown ids are skipped with a warning |
| `[status_line].command` | `string` | `""` | Custom status line command. Its first stdout line replaces the first footer line, with a JSON snapshot (model, provider, cwd, git branch, permission mode, plan mode, context usage, session id, version) passed on stdin. Runs are capped at 300ms and throttled to once per second; failures fall back to the built-in layout |

The `provider` slot shows which provider serves the current model (`managed:` ids
render without the prefix, e.g. `openrouter`), which disambiguates look-alike model
names across providers. It is opt-in: list it in `items` to enable it.

```toml
# ~/.kimi-code/tui.toml
Expand All @@ -411,7 +415,7 @@ notification_condition = "unfocused" # "unfocused" | "always"
auto_install = true

# [status_line]
# items = ["mode", "goal", "model", "tasks", "cwd", "git", "tips"]
# items = ["mode", "goal", "provider", "model", "tasks", "cwd", "git", "tips"]
# command = "~/.kimi-code/statusline.sh"
```

Expand Down
10 changes: 7 additions & 3 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,12 @@ MCP server 的声明配置写在 `~/.kimi-code/mcp.json` 或项目内 `.kimi-cod
| `[notifications].enabled` | `boolean` | `true` | 是否发送桌面通知 |
| `[notifications].notification_condition` | `string` | `unfocused` | 何时通知:`unfocused`(仅终端失去焦点时)或 `always`(总是) |
| `[upgrade].auto_install` | `boolean` | `true` | 是否自动安装新版本 |
| `[status_line].items` | `string[]` | `[]` | 底部状态栏第一行展示哪些内置槽位及其顺序:`mode`、`goal`、`model`、`tasks`、`cwd`、`git`、`tips`。缺省保持默认布局;未知 id 跳过并告警 |
| `[status_line].command` | `string` | `""` | 自定义状态栏命令。其 stdout 第一行替换状态栏第一行,stdin 会收到 JSON 快照(model、cwd、git 分支、permission 模式、plan 模式、上下文用量、session id、版本)。运行上限 300ms、每秒最多一次;失败时回退内置布局 |
| `[status_line].items` | `string[]` | `[]` | 底部状态栏第一行展示哪些内置槽位及其顺序:`mode`、`goal`、`provider`、`model`、`tasks`、`cwd`、`git`、`tips`。缺省保持默认布局(默认不含 `provider`);未知 id 跳过并告警 |
| `[status_line].command` | `string` | `""` | 自定义状态栏命令。其 stdout 第一行替换状态栏第一行,stdin 会收到 JSON 快照(model、provider、cwd、git 分支、permission 模式、plan 模式、上下文用量、session id、版本)。运行上限 300ms、每秒最多一次;失败时回退内置布局 |

`provider` 槽位显示当前模型由哪个提供商提供(`managed:` 前缀不展示,例如显示为
`openrouter`),便于区分不同提供商下同名或相似的模型。该槽位需要显式在 `items`
中列出才会启用。

```toml
# ~/.kimi-code/tui.toml
Expand All @@ -411,7 +415,7 @@ notification_condition = "unfocused" # "unfocused" | "always"
auto_install = true

# [status_line]
# items = ["mode", "goal", "model", "tasks", "cwd", "git", "tips"]
# items = ["mode", "goal", "provider", "model", "tasks", "cwd", "git", "tips"]
# command = "~/.kimi-code/statusline.sh"
```

Expand Down