diff --git a/.changeset/auto-session-title.md b/.changeset/auto-session-title.md new file mode 100644 index 000000000..e7715103d --- /dev/null +++ b/.changeset/auto-session-title.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Automatically generate concise session titles after the first turn without overwriting custom titles. 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 ff30b5e50..9feb7cf17 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -393,22 +393,28 @@ export class SessionEventHandler { } /** - * Best-effort auto title: while the session has no title yet, ask the - * engine to generate one from the first prompts after each completed turn. - * The engine keeps custom titles untouched and dedupes in-flight requests; - * a resolved `undefined` (no managed login / no prompt yet) just retries on - * the next turn, while a rejection (v1 engine, dead RPC) disables further - * attempts for this session. The generated title lands through the regular - * `session.meta.updated` event. + * Best-effort auto title: after each completed turn, ask the engine to + * generate a title from the first prompts until one lands. The engine + * overwrites the prompt-derived easy title but never a custom title + * (enforced server-side), and dedupes in-flight requests. A resolved string + * means a title was applied — stop asking so later turns don't regenerate + * over it; a resolved `undefined` (no managed login / no prompt yet / a + * custom title) just retries on the next turn; a rejection (v1 engine, + * dead RPC) disables further attempts for this session. The generated + * title lands through the regular `session.meta.updated` event. */ private requestSessionTitleGeneration(): void { if (this.titleGenerationDisabled) return; - const { sessionId, sessionTitle } = this.host.state.appState; + const { sessionId } = this.host.state.appState; if (sessionId.length === 0) return; - if (typeof sessionTitle === 'string' && sessionTitle.trim().length > 0) return; - void this.host.harness.generateSessionTitle({ id: sessionId }).catch(() => { - this.titleGenerationDisabled = true; - }); + void this.host.harness.generateSessionTitle({ id: sessionId }).then( + (title) => { + if (title !== undefined) this.titleGenerationDisabled = true; + }, + () => { + this.titleGenerationDisabled = true; + }, + ); } private handleStepBegin(event: TurnStepStartedEvent): void { diff --git a/apps/kimi-code/test/tui/controllers/session-event-handler-title.test.ts b/apps/kimi-code/test/tui/controllers/session-event-handler-title.test.ts index 7fb56e5ff..c5de0435d 100644 --- a/apps/kimi-code/test/tui/controllers/session-event-handler-title.test.ts +++ b/apps/kimi-code/test/tui/controllers/session-event-handler-title.test.ts @@ -84,13 +84,24 @@ describe('session auto title generation', () => { expect(harness.generateSessionTitle).toHaveBeenCalledWith({ id: 's1' }); }); - it('does not request a title when the session already has one', () => { - const { host, harness } = makeHost({ sessionTitle: 'custom title' }); + it('requests a title after a turn ends even when an easy title exists', () => { + const { host, harness } = makeHost({ sessionTitle: '首条 prompt 的截断标题' }); const handler = new SessionEventHandler(host); handler.handleEvent(turnEndedEvent(), vi.fn()); - expect(harness.generateSessionTitle).not.toHaveBeenCalled(); + expect(harness.generateSessionTitle).toHaveBeenCalledWith({ id: 's1' }); + }); + + it('stops requesting after a title was generated', async () => { + const { host, harness } = makeHost({ generateTitle: async () => '生成的标题' }); + const handler = new SessionEventHandler(host); + + handler.handleEvent(turnEndedEvent(), vi.fn()); + await flushMicrotasks(); + handler.handleEvent(turnEndedEvent(), vi.fn()); + + expect(harness.generateSessionTitle).toHaveBeenCalledTimes(1); }); it('keeps requesting after an unavailable (undefined) result', async () => {