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/title-footer-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add the /titleon and /titleoff slash commands to toggle showing the current session title in the footer after the git branch. The toggle is session-only: it is not persisted to tui.toml and resets when switching to another session or starting a new one.
10 changes: 10 additions & 0 deletions apps/kimi-code/src/tui/commands/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import {
handleForkCommand,
handleInitCommand,
handleTitleCommand,
handleTitleOffCommand,
handleTitleOnCommand,
} from './session';
import { handleSwarmCommand } from './swarm';
import { handleUndoCommand } from './undo';
Expand Down Expand Up @@ -89,6 +91,8 @@ export {
handleForkCommand,
handleInitCommand,
handleTitleCommand,
handleTitleOffCommand,
handleTitleOnCommand,
} from './session';
export { handleUndoCommand } from './undo';
export { handleWebCommand } from './web';
Expand Down Expand Up @@ -330,6 +334,12 @@ async function handleBuiltInSlashCommand(
case 'title':
await handleTitleCommand(host, args);
return;
case 'titleon':
handleTitleOnCommand(host);
return;
case 'titleoff':
handleTitleOffCommand(host);
return;
case 'yolo':
await handleYoloCommand(host, args);
return;
Expand Down
8 changes: 7 additions & 1 deletion apps/kimi-code/src/tui/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ export { handlePluginsCommand } from './plugins';
export { handleReloadCommand, handleReloadTuiCommand } from './reload';
export { handleGoalCommand, parseGoalCommand } from './goal';
export { goalArgumentCompletions } from './registry';
export { handleForkCommand, handleInitCommand, handleTitleCommand } from './session';
export {
handleForkCommand,
handleInitCommand,
handleTitleCommand,
handleTitleOffCommand,
handleTitleOnCommand,
} from './session';
export { handleUndoCommand } from './undo';
export { handleWebCommand } from './web';
export {
Expand Down
14 changes: 14 additions & 0 deletions apps/kimi-code/src/tui/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,20 @@ export const BUILTIN_SLASH_COMMANDS = [
argumentHint: '<title>',
availability: 'always',
},
{
name: 'titleon',
aliases: [],
description: 'Show session title in the footer (this session only)',
priority: 60,
availability: 'always',
},
{
name: 'titleoff',
aliases: [],
description: 'Hide session title from the footer',
priority: 60,
availability: 'always',
},
{
name: 'usage',
aliases: [],
Expand Down
34 changes: 34 additions & 0 deletions apps/kimi-code/src/tui/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,40 @@ export async function handleTitleCommand(host: SlashCommandHost, args: string):
host.showStatus(`Session title set to: ${newTitle}`);
}

// Narrowed host for the /titleon and /titleoff footer toggles (mirrors the
// UpdatePreferenceHost pattern in config.ts) so tests can use a plain fake.
type SessionTitleFooterHost = {
readonly state: {
readonly appState: Pick<
SlashCommandHost['state']['appState'],
'showSessionTitleInFooter'
>;
};
setAppState(patch: Pick<SlashCommandHost['state']['appState'], 'showSessionTitleInFooter'>): void;
showStatus(msg: string, color?: string): void;
track: SlashCommandHost['track'];
};

export function handleTitleOnCommand(host: SessionTitleFooterHost): void {
if (host.state.appState.showSessionTitleInFooter) {
host.showStatus('Session title is already shown in the footer.');
return;
}
host.setAppState({ showSessionTitleInFooter: true });
host.track('session_title_footer_changed', { visible: true });
host.showStatus('Session title now shown in the footer (this session only).');
}

export function handleTitleOffCommand(host: SessionTitleFooterHost): void {
if (!host.state.appState.showSessionTitleInFooter) {
host.showStatus('Session title is already hidden from the footer.');
return;
}
host.setAppState({ showSessionTitleInFooter: false });
host.track('session_title_footer_changed', { visible: false });
host.showStatus('Session title hidden from the footer.');
}

export async function handleForkCommand(host: SlashCommandHost, args: string): Promise<void> {
void args;
const session = host.session;
Expand Down
12 changes: 12 additions & 0 deletions apps/kimi-code/src/tui/components/chrome/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from '#/utils/usage/usage-format';

const MAX_CWD_SEGMENTS = 3;
const MAX_FOOTER_TITLE_LENGTH = 40;
const GOAL_TIMER_INTERVAL_MS = 1_000;

// Toolbar tips — rotates every 10s. Most tips are short and pair up (two
Expand Down Expand Up @@ -308,6 +309,17 @@ export class FooterComponent implements Component {
left.push(formatFooterGitBadge(git, colors));
}

if (state.showSessionTitleInFooter && state.sessionTitle) {
// /title accepts pasted multiline text; keep the footer on one line
// (same collapse as the session picker's singleLine).
const title = state.sessionTitle.replaceAll(/\s+/g, ' ').trim();
if (title.length > 0) {
left.push(
chalk.hex(colors.textDim)(truncateToWidth(title, MAX_FOOTER_TITLE_LENGTH, '…')),
);
}
}

const leftLine = left.join(' ');
const leftWidth = visibleWidth(leftLine);

Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/src/tui/controllers/auth-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class AuthFlowController {
maxContextTokens: 0,
contextUsage: 0,
sessionTitle: null,
showSessionTitleInFooter: false,
});
this.host.appendStartupNotice(OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE);
this.host.setStartupReady();
Expand Down Expand Up @@ -110,6 +111,7 @@ export class AuthFlowController {
sessionId: '',
model: '',
sessionTitle: null,
showSessionTitleInFooter: false,
});
await this.host.refreshSkillCommands();
await this.host.refreshPluginCommands();
Expand Down
6 changes: 5 additions & 1 deletion apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ function createInitialAppState(input: KimiTUIStartupInput): AppState {
availableModels: {},
availableProviders: {},
sessionTitle: null,
showSessionTitleInFooter: false,
goal: null,
mcpServersSummary: null,
banner: undefined,
Expand Down Expand Up @@ -1736,6 +1737,8 @@ export class KimiTUI {
async switchToSession(session: Session, statusMessage: string): Promise<void> {
this.resetSessionRuntime();
await this.setSession(session);
// /titleon is session-only: a session switch (including /fork) resets it.
this.setAppState({ showSessionTitleInFooter: false });
await this.syncRuntimeState(session);
this.updateTerminalTitle();
try {
Expand Down Expand Up @@ -1808,7 +1811,8 @@ export class KimiTUI {

this.resetSessionRuntime();
await this.setSession(session);
this.setAppState({ sessionId: session.id });
// /titleon is session-only: a new session resets it.
this.setAppState({ sessionId: session.id, showSessionTitleInFooter: false });
try {
await this.activateRuntime();
await this.syncRuntimeState(session);
Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-code/src/tui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export interface AppState {
availableModels: Record<string, ModelAlias>;
availableProviders: Record<string, ProviderConfig>;
sessionTitle: string | null;
/** Session-only toggle (/titleon, /titleoff): show the session title in the
* footer after the git badge. Not persisted; resets on session switch. */
showSessionTitleInFooter: boolean;
/** Current goal snapshot for the footer badge; null/undefined when no active goal. */
goal?: GoalSnapshot | null;
mcpServersSummary: string | null;
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/test/tui/commands/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ describe('built-in slash command registry', () => {
'status',
'theme',
'title',
'titleoff',
'titleon',
'undo',
'usage',
'version',
Expand Down
64 changes: 64 additions & 0 deletions apps/kimi-code/test/tui/commands/session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it, vi } from 'vitest';

import { handleTitleOffCommand, handleTitleOnCommand } from '#/tui/commands/session';

function fakeHost(showSessionTitleInFooter: boolean) {
return {
state: {
appState: { showSessionTitleInFooter },
},
setAppState: vi.fn(),
showStatus: vi.fn(),
track: vi.fn(),
};
}

describe('/titleon', () => {
it('enables the footer session title for the current session only', () => {
const host = fakeHost(false);

handleTitleOnCommand(host);

expect(host.setAppState).toHaveBeenCalledWith({ showSessionTitleInFooter: true });
expect(host.track).toHaveBeenCalledWith('session_title_footer_changed', { visible: true });
expect(host.showStatus).toHaveBeenCalledWith(
'Session title now shown in the footer (this session only).',
);
});

it('is idempotent when the title is already shown', () => {
const host = fakeHost(true);

handleTitleOnCommand(host);

expect(host.setAppState).not.toHaveBeenCalled();
expect(host.track).not.toHaveBeenCalled();
expect(host.showStatus).toHaveBeenCalledWith(
'Session title is already shown in the footer.',
);
});
});

describe('/titleoff', () => {
it('disables the footer session title', () => {
const host = fakeHost(true);

handleTitleOffCommand(host);

expect(host.setAppState).toHaveBeenCalledWith({ showSessionTitleInFooter: false });
expect(host.track).toHaveBeenCalledWith('session_title_footer_changed', { visible: false });
expect(host.showStatus).toHaveBeenCalledWith('Session title hidden from the footer.');
});

it('is idempotent when the title is already hidden', () => {
const host = fakeHost(false);

handleTitleOffCommand(host);

expect(host.setAppState).not.toHaveBeenCalled();
expect(host.track).not.toHaveBeenCalled();
expect(host.showStatus).toHaveBeenCalledWith(
'Session title is already hidden from the footer.',
);
});
});
Loading