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
4 changes: 4 additions & 0 deletions apps/kimi-code/src/tui/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,18 @@ export async function showStatusReport(host: SlashCommandHost): Promise<void> {
thinking: appState.thinking,
permissionMode: appState.permissionMode,
planMode: appState.planMode,
swarmMode: appState.swarmMode,
contextUsage: appState.contextUsage,
contextTokens: appState.contextTokens,
maxContextTokens: appState.maxContextTokens,
availableModels: appState.availableModels,
availableProviders: appState.availableProviders,
status: runtimeStatus.status,
statusError: runtimeStatus.error,
managedUsage: managedUsage?.usage,
managedUsageError: managedUsage?.error,
mcpServersSummary: appState.mcpServersSummary,
goal: appState.goal,
};
const panel = new UsagePanelComponent(() => buildStatusReportLines(reportArgs), 'primary', ' Status ');
host.state.transcriptContainer.addChild(panel);
Expand Down
46 changes: 43 additions & 3 deletions apps/kimi-code/src/tui/components/messages/status-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* separate from the TUI orchestration layer.
*/

import type { ModelAlias, PermissionMode, SessionStatus } from '@moonshot-ai/kimi-code-sdk';
import type { GoalSnapshot, ModelAlias, PermissionMode, ProviderConfig, SessionStatus } from '@moonshot-ai/kimi-code-sdk';

import { PRODUCT_NAME } from '#/constant/app';
import { currentTheme } from '#/tui/theme';
Expand Down Expand Up @@ -33,14 +33,18 @@ export interface StatusReportOptions {
readonly thinking: boolean;
readonly permissionMode: PermissionMode;
readonly planMode: boolean;
readonly swarmMode: boolean;
readonly contextUsage: number;
readonly contextTokens: number;
readonly maxContextTokens: number;
readonly availableModels: Record<string, ModelAlias>;
readonly availableProviders: Record<string, ProviderConfig>;
readonly status?: SessionStatus;
readonly statusError?: string;
readonly managedUsage?: ManagedUsageReport;
readonly managedUsageError?: string;
readonly mcpServersSummary?: string | null;
readonly goal?: GoalSnapshot | null;
}

type Colorize = (text: string) => string;
Expand All @@ -60,6 +64,26 @@ function formatModelStatus(options: StatusReportOptions): string {
return `${displayModelName(model, options.availableModels)} (thinking ${thinking})`;
}

function formatPermissionMode(permission: PermissionMode, value: Colorize, errorStyle: Colorize): string {
if (permission === 'yolo') return errorStyle('yolo');
if (permission === 'auto') return value('auto');
return value(permission);
}

function formatPlanMode(planMode: boolean, value: Colorize, muted: Colorize): string {
return planMode ? value('on') : muted('off');
}

function formatSwarmMode(swarmMode: boolean, value: Colorize, muted: Colorize): string {
return swarmMode ? value('on') : muted('off');
}

function formatGoalStatus(goal: GoalSnapshot | null | undefined): string | undefined {
if (goal === null || goal === undefined) return undefined;
const objective = goal.objective.length > 40 ? `${goal.objective.slice(0, 40)}…` : goal.objective;
return `${objective} · ${goal.status}`;
}

function addFieldRows(
lines: string[],
rows: readonly FieldRow[],
Expand Down Expand Up @@ -97,15 +121,31 @@ export function buildStatusReportLines(options: StatusReportOptions): string[] {
const permission = options.status?.permission ?? options.permissionMode;
const planMode = options.status?.planMode ?? options.planMode;
const sessionId = options.sessionId.trim().length > 0 ? options.sessionId : 'none';
const modelCount = Object.keys(options.availableModels).length;
const providerCount = Object.keys(options.availableProviders).length;
const mcpSummary = options.mcpServersSummary?.trim();
const goalStatus = formatGoalStatus(options.goal);

const rows: FieldRow[] = [
{ label: 'Model', value: formatModelStatus(options) },
{ label: 'Directory', value: options.workDir },
{ label: 'Permissions', value: permission },
{ label: 'Plan mode', value: planMode ? 'on' : 'off' },
{ label: 'Permissions', value: formatPermissionMode(permission, value, errorStyle) },
{ label: 'Plan mode', value: formatPlanMode(planMode, value, muted) },
{ label: 'Swarm', value: formatSwarmMode(options.swarmMode, value, muted) },

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 Read swarm mode from runtime status

When /status successfully loads runtime status it uses that snapshot for permissions, plan mode, model, and context, but the new Swarm row always reads the cached app state. In contexts where app state is stale while session.getStatus() has the current value (the SDK status includes swarmMode in packages/node-sdk/src/rpc.ts), the status card can report Swarm off even though the active session is in swarm mode. Please derive this row from options.status?.swarmMode ?? options.swarmMode, matching the other runtime-backed fields.

Useful? React with 👍 / 👎.

{ label: 'Session', value: sessionId },
];
const title = options.sessionTitle?.trim();
if (title !== undefined && title.length > 0) rows.push({ label: 'Title', value: title });
if (goalStatus !== undefined) {
rows.push({ label: 'Goal', value: goalStatus });
}
rows.push(
{ label: 'Providers', value: `${providerCount}` },
{ label: 'Models', value: `${modelCount}` },
);
if (mcpSummary !== undefined && mcpSummary.length > 0) {
rows.push({ label: 'MCP servers', value: mcpSummary });
}
if (options.statusError !== undefined) {
rows.push({ label: 'Warning', value: options.statusError, severity: 'error' });
}
Expand Down
36 changes: 35 additions & 1 deletion apps/kimi-code/test/tui/components/messages/status-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('status panel report lines', () => {
thinking: true,
permissionMode: 'manual',
planMode: false,
swarmMode: false,
contextUsage: 0.25,
contextTokens: 2500,
maxContextTokens: 10000,
Expand All @@ -28,6 +29,33 @@ describe('status panel report lines', () => {
displayName: 'Kimi K2',
},
},
availableProviders: {
'managed:kimi-code': {
type: 'kimi',
apiKey: 'sk-test',
},
},
mcpServersSummary: '2 connected',
goal: {
goalId: 'goal-1',
objective: 'Ship the status card',
status: 'active',
turnsUsed: 3,
tokensUsed: 1200,
wallClockMs: 45000,
budget: {
tokenBudget: null,
turnBudget: null,
wallClockBudgetMs: null,
remainingTokens: null,
remainingTurns: null,
remainingWallClockMs: null,
tokenBudgetReached: false,
turnBudgetReached: false,
wallClockBudgetReached: false,
overBudget: false,
},
},
status: {
model: 'k2',
thinkingLevel: 'high',
Expand Down Expand Up @@ -56,16 +84,20 @@ describe('status panel report lines', () => {
expect(output).toContain('Directory /tmp/project');
expect(output).toContain('Permissions auto');
expect(output).toContain('Plan mode on');
expect(output).toContain('Swarm off');
expect(output).toContain('Session ses-1');
expect(output).toContain('Title Implement status');
expect(output).toContain('Goal Ship the status card · active');
expect(output).toContain('Providers 1');
expect(output).toContain('Models 1');
expect(output).toContain('MCP servers 2 connected');
expect(output).toContain('Context window');
expect(output).toContain('25.0%');
expect(output).toContain('(3.0k / 12.0k)');
expect(output).toContain('Plan usage');
expect(output).toContain('8% used');
expect(output).not.toContain('Account');
expect(output).not.toContain('AGENTS.md');
expect(output).not.toContain('Runtime');
});

it('falls back to app state and shows status load errors as warnings', () => {
Expand All @@ -78,10 +110,12 @@ describe('status panel report lines', () => {
thinking: false,
permissionMode: 'manual',
planMode: false,
swarmMode: false,
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
availableModels: {},
availableProviders: {},
statusError: 'No active session',
}).map(strip);

Expand Down