From e2d1922ec796c79036d4b701f985b1016d6cc4ba Mon Sep 17 00:00:00 2001 From: liruifengv Date: Thu, 4 Jun 2026 11:27:03 +0800 Subject: [PATCH] fix(tui): set terminal title without renaming process --- .changeset/fix-terminal-title-process-name.md | 5 +++ apps/kimi-code/src/main.ts | 2 -- apps/kimi-code/src/tui/constant/terminal.ts | 2 +- .../src/tui/controllers/auth-flow.ts | 4 +-- .../tui/controllers/session-event-handler.ts | 5 +-- apps/kimi-code/src/tui/kimi-tui.ts | 13 +++++--- apps/kimi-code/src/tui/utils/proctitle.ts | 29 ----------------- apps/kimi-code/src/utils/process/proctitle.ts | 31 ------------------- apps/kimi-code/test/cli/main.test.ts | 12 +++++++ .../test/tui/kimi-tui-message-flow.test.ts | 29 ++++++++++++++++- 10 files changed, 59 insertions(+), 73 deletions(-) create mode 100644 .changeset/fix-terminal-title-process-name.md delete mode 100644 apps/kimi-code/src/tui/utils/proctitle.ts delete mode 100644 apps/kimi-code/src/utils/process/proctitle.ts diff --git a/.changeset/fix-terminal-title-process-name.md b/.changeset/fix-terminal-title-process-name.md new file mode 100644 index 0000000000..e126389078 --- /dev/null +++ b/.changeset/fix-terminal-title-process-name.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Set terminal tab titles without renaming the running process. diff --git a/apps/kimi-code/src/main.ts b/apps/kimi-code/src/main.ts index e1251be2fd..a4330586b0 100644 --- a/apps/kimi-code/src/main.ts +++ b/apps/kimi-code/src/main.ts @@ -36,7 +36,6 @@ import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from './constant/app'; import { cleanupStaleNativeCacheForCurrent } from './native/native-assets'; import { installNativeModuleHook } from './native/module-hook'; import { runNativeAssetSmokeIfRequested } from './native/smoke'; -import { initProcessName } from './utils/process/proctitle'; export async function handleMainCommand(opts: CLIOptions, version: string): Promise { let validated: ReturnType; @@ -116,7 +115,6 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = { }; export function main(): void { - initProcessName(); installCrashHandlers(); installNativeModuleHook(); if (runNativeAssetSmokeIfRequested()) return; diff --git a/apps/kimi-code/src/tui/constant/terminal.ts b/apps/kimi-code/src/tui/constant/terminal.ts index 3530d19b30..ec87a07803 100644 --- a/apps/kimi-code/src/tui/constant/terminal.ts +++ b/apps/kimi-code/src/tui/constant/terminal.ts @@ -33,7 +33,7 @@ export const OSC11_RESPONSE_PREFIX_NO_ESC = "]11;rgb:"; // Keep notification/title payloads bounded so terminal tabs and desktop // notifications stay readable. export const MAX_TERMINAL_NOTIFICATION_MESSAGE_LENGTH = 240; -export const MAX_PROCESS_TITLE_LENGTH = 32; +export const MAX_TERMINAL_TITLE_LENGTH = 32; // OSC 11 probing must be short because unsupported terminals do not reply. export const TERMINAL_THEME_DETECT_TIMEOUT_MS = 250; diff --git a/apps/kimi-code/src/tui/controllers/auth-flow.ts b/apps/kimi-code/src/tui/controllers/auth-flow.ts index 4f1fdc1040..39af925fbd 100644 --- a/apps/kimi-code/src/tui/controllers/auth-flow.ts +++ b/apps/kimi-code/src/tui/controllers/auth-flow.ts @@ -22,7 +22,7 @@ export interface AuthFlowHost { appendStartupNotice(extra: string): void; readonly sessionEventHandler: SessionEventHandler; fetchSessions(): Promise; - refreshSessionTitle(): void; + updateTerminalTitle(): void; refreshSkillCommands(session?: SkillListSession): Promise; } @@ -82,7 +82,7 @@ export class AuthFlowController { await host.syncRuntimeState(session); host.sessionEventHandler.startSubscription(); void host.fetchSessions(); - host.refreshSessionTitle(); + host.updateTerminalTitle(); void host.refreshSkillCommands(host.session); } diff --git a/apps/kimi-code/src/tui/controllers/session-event-handler.ts b/apps/kimi-code/src/tui/controllers/session-event-handler.ts index 36753a6cbb..c5184b801c 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -58,7 +58,6 @@ import { selectMcpStartupStatusRows, } from '../utils/mcp-server-status'; import { openUrl } from '#/utils/open-url'; -import { setProcessTitle } from '../utils/proctitle'; import { errorReportHintLine } from '../constant/feedback'; import { formatStepDebugTiming } from '#/utils/usage/debug-timing'; import { nextTranscriptId } from '../utils/transcript-id'; @@ -91,6 +90,7 @@ export interface SessionEventHost { showStatus(msg: string, color?: string): void; showNotice(title: string, detail?: string): void; appendTranscriptEntry(entry: TranscriptEntry): void; + updateTerminalTitle(): void; sendQueuedMessage(session: Session, item: QueuedMessage): void; shiftQueuedMessage(): QueuedMessage | undefined; readonly btwPanelController: BtwPanelController; @@ -296,6 +296,7 @@ export class SessionEventHandler { case 'cron.fired': case 'error': case 'warning': + case 'goal.updated': case 'session.meta.updated': case 'skill.activated': case 'subagent.completed': @@ -592,7 +593,7 @@ export class SessionEventHandler { const title = event.title ?? stringValue(event.patch?.['title']); if (title !== undefined) { this.host.setAppState({ sessionTitle: title }); - setProcessTitle(title, this.host.state.appState.sessionId); + this.host.updateTerminalTitle(); } } diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 6c6bec8e38..db152fe271 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -86,7 +86,9 @@ import { LLM_NOT_SET_MESSAGE, MAIN_AGENT_ID, NO_ACTIVE_SESSION_MESSAGE, + PRODUCT_NAME, } from './constant/kimi-tui'; +import { MAX_TERMINAL_TITLE_LENGTH } from './constant/terminal'; import { combineStartupNotice, isOAuthLoginRequiredError } from './utils/startup'; import { adaptPanelResponse } from './reverse-rpc/approval/adapter'; import { ApprovalController } from './reverse-rpc/approval/controller'; @@ -117,7 +119,6 @@ import { ImageAttachmentStore, type ImageAttachment } from './utils/image-attach import { extractMediaAttachments } from './utils/image-placeholder'; import { hasPatchChanges } from './utils/object-patch'; import { openUrl } from '#/utils/open-url'; -import { setProcessTitle } from './utils/proctitle'; import { sessionRowsForPicker } from './utils/session-picker-rows'; import { installTerminalFocusTracking } from './utils/terminal-focus'; import { notifyTerminalOnce } from './utils/terminal-notification'; @@ -462,7 +463,7 @@ export class KimiTUI { } void this.fetchSessions(); if (this.session !== undefined) { - this.refreshSessionTitle(); + this.updateTerminalTitle(); } void this.refreshSkillCommands(this.session); } @@ -1090,8 +1091,10 @@ export class KimiTUI { } } - refreshSessionTitle(): void { - setProcessTitle(this.state.appState.sessionTitle, this.state.appState.sessionId); + updateTerminalTitle(): void { + const trimmed = this.state.appState.sessionTitle?.trim() ?? ''; + const label = trimmed.length > 0 ? trimmed.slice(0, MAX_TERMINAL_TITLE_LENGTH) : PRODUCT_NAME; + this.state.terminal.setTitle(label); } resetSessionRuntime(): void { @@ -1144,7 +1147,7 @@ export class KimiTUI { this.resetSessionRuntime(); await this.setSession(session); await this.syncRuntimeState(session); - this.refreshSessionTitle(); + this.updateTerminalTitle(); try { await this.refreshSkillCommands(this.session); } catch { diff --git a/apps/kimi-code/src/tui/utils/proctitle.ts b/apps/kimi-code/src/tui/utils/proctitle.ts deleted file mode 100644 index c9c30d823b..0000000000 --- a/apps/kimi-code/src/tui/utils/proctitle.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Terminal window title synchronization. - * - * Uses the session title when present, capped at 80 characters to keep tabs - * readable. New or unnamed sessions fall back to `Kimi Code`. - * - * Writes both `process.title`, for process listings, and OSC 0/2 escape - * sequences, which most terminals use for window/tab titles. Non-TTY stdout - * skips the OSC write. - */ -import { PRODUCT_NAME } from '#/constant/app'; -import { MAX_PROCESS_TITLE_LENGTH } from '#/tui/constant/terminal'; - -export function setProcessTitle(title: string | null, _sessionId: string): void { - const trimmed = title?.trim() ?? ''; - const label = trimmed.length > 0 ? trimmed.slice(0, MAX_PROCESS_TITLE_LENGTH) : PRODUCT_NAME; - try { - process.title = label; - } catch { - /* noop */ - } - try { - if (process.stdout.isTTY) { - process.stdout.write(`\u001B]0;${label}\u0007`); - } - } catch { - /* noop */ - } -} diff --git a/apps/kimi-code/src/utils/process/proctitle.ts b/apps/kimi-code/src/utils/process/proctitle.ts deleted file mode 100644 index b731233af6..0000000000 --- a/apps/kimi-code/src/utils/process/proctitle.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Early-startup process name initialization. - * - * Sets the process title so `ps`/`top` and the terminal tab show - * `Kimi Code` from the moment the binary launches — before Commander - * parses argv, before any preflight, even on `--help`/`--version`. - * - * OSC is written to stderr (not stdout) so it still reaches the terminal - * when stdout is piped, e.g. `kimi --print | grep ...`. - */ -import { PRODUCT_NAME } from '#/constant/app'; -import { BEL, ESC } from '#/constant/terminal'; - -export function setProcessTitle(label: string): void { - try { - process.title = label; - } catch { - /* noop */ - } - try { - if (process.stderr.isTTY) { - process.stderr.write(`${ESC}]0;${label}${BEL}`); - } - } catch { - /* noop */ - } -} - -export function initProcessName(name: string = PRODUCT_NAME): void { - setProcessTitle(name); -} diff --git a/apps/kimi-code/test/cli/main.test.ts b/apps/kimi-code/test/cli/main.test.ts index 4a5ac62ad3..7c0817a093 100644 --- a/apps/kimi-code/test/cli/main.test.ts +++ b/apps/kimi-code/test/cli/main.test.ts @@ -260,6 +260,18 @@ describe('main entry command handling', () => { expect(mocks.parse).toHaveBeenCalledWith(process.argv); }); + it('does not rewrite the process title during startup', () => { + const originalTitle = process.title; + try { + process.title = 'kimi-test-runner'; + main(); + + expect(process.title).toBe('kimi-test-runner'); + } finally { + process.title = originalTitle; + } + }); + it('exits early when update preflight requests process exit', async () => { const opts = defaultOpts(); mocks.validateOptions.mockReturnValue({ options: opts, uiMode: 'shell' }); diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 3b6ab6b623..644e0ebf39 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -2809,6 +2809,30 @@ describe('KimiTUI message flow', () => { expect(write).toHaveBeenCalledWith(deleteAllKittyImages()); }); + it('updates terminal title through pi-tui without changing process title', async () => { + const originalTitle = process.title; + const { driver } = await makeDriver(makeSession({ id: 'ses-1' })); + const setTitle = vi.spyOn(driver.state.terminal, 'setTitle').mockImplementation(() => {}); + + try { + process.title = 'kimi-test-runner'; + driver.sessionEventHandler.handleEvent( + { + type: 'session.meta.updated', + sessionId: 'ses-1', + agentId: 'main', + title: 'Implement terminal title', + } as Event, + () => {}, + ); + + expect(setTitle).toHaveBeenCalledWith('Implement terminal title'); + expect(process.title).toBe('kimi-test-runner'); + } finally { + process.title = originalTitle; + } + }); + it('forks the active session and switches to the returned session', async () => { const originalTitle = process.title; const source = makeSession({ @@ -2821,8 +2845,10 @@ describe('KimiTUI message flow', () => { }); const forkSession = vi.fn(async () => forked); const { driver, harness } = await makeDriver(source, { forkSession }); + const setTitle = vi.spyOn(driver.state.terminal, 'setTitle').mockImplementation(() => {}); try { + process.title = 'kimi-test-runner'; driver.handleUserInput('/fork ignored args'); await vi.waitFor(() => { @@ -2832,7 +2858,8 @@ describe('KimiTUI message flow', () => { }); expect(driver.getCurrentSessionId()).toBe('ses-fork'); }); - expect(process.title).toBe('Fork: Source title'); + expect(setTitle).toHaveBeenCalledWith('Fork: Source title'); + expect(process.title).toBe('kimi-test-runner'); expect(source.close).toHaveBeenCalledOnce(); expect(forked.onEvent).toHaveBeenCalledOnce(); expect(harness.resumeSession).not.toHaveBeenCalled();