Skip to content

Commit e1c0c1d

Browse files
authored
Merge pull request #4590 from Kilo-Org/feat/session-title-generated-event
2 parents a3988cd + 9eb185a commit e1c0c1d

File tree

9 files changed

+125
-5
lines changed

9 files changed

+125
-5
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"kilo-code": patch
3+
"@kilocode/cli": patch
4+
---
5+
6+
feat: add session_title_generated event emission to CLI

cli/src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ export class CLI {
169169
console.log(JSON.stringify(message))
170170
}
171171
},
172+
onSessionTitleGenerated: (message) => {
173+
if (this.options.json) {
174+
console.log(JSON.stringify(message))
175+
}
176+
},
172177
platform: "cli",
173178
getOrganizationId: async () => {
174179
const state = this.service?.getState()

src/core/kilocode/agent-manager/CliOutputParser.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ export interface SessionCreatedStreamEvent {
7171
timestamp: number
7272
}
7373

74+
export interface SessionTitleGeneratedStreamEvent {
75+
streamEventType: "session_title_generated"
76+
sessionId: string
77+
title: string
78+
timestamp: number
79+
}
80+
7481
export interface WelcomeStreamEvent {
7582
streamEventType: "welcome"
7683
worktreeBranch?: string
@@ -85,6 +92,7 @@ export type StreamEvent =
8592
| CompleteStreamEvent
8693
| InterruptedStreamEvent
8794
| SessionCreatedStreamEvent
95+
| SessionTitleGeneratedStreamEvent
8896
| WelcomeStreamEvent
8997

9098
/**
@@ -223,6 +231,16 @@ function toStreamEvent(parsed: Record<string, unknown>): StreamEvent | null {
223231
}
224232
}
225233

234+
// Detect session_title_generated event from CLI (format: { event: "session_title_generated", sessionId: "...", title: "...", timestamp: ... })
235+
if (parsed.event === "session_title_generated" && typeof parsed.sessionId === "string" && typeof parsed.title === "string") {
236+
return {
237+
streamEventType: "session_title_generated",
238+
sessionId: parsed.sessionId as string,
239+
title: parsed.title as string,
240+
timestamp: (parsed.timestamp as number) || Date.now(),
241+
}
242+
}
243+
226244
// Detect welcome event from CLI (format: { type: "welcome", metadata: { welcomeOptions: { worktreeBranch: "..." } }, ... })
227245
if (parsed.type === "welcome") {
228246
const metadata = parsed.metadata as Record<string, unknown> | undefined

src/core/kilocode/agent-manager/__tests__/CliOutputParser.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,37 @@ describe("parseCliChunk", () => {
7373
expect(event.timestamp).toBeLessThanOrEqual(after)
7474
})
7575

76+
it("should parse session_title_generated event from CLI", () => {
77+
const result = parseCliChunk(
78+
'{"event":"session_title_generated","sessionId":"sess-abc-123","title":"My Session Title","timestamp":1234567890}\n',
79+
)
80+
expect(result.events).toHaveLength(1)
81+
expect(result.events[0]).toEqual({
82+
streamEventType: "session_title_generated",
83+
sessionId: "sess-abc-123",
84+
title: "My Session Title",
85+
timestamp: 1234567890,
86+
})
87+
})
88+
89+
it("should use current timestamp when session_title_generated has no timestamp", () => {
90+
const before = Date.now()
91+
const result = parseCliChunk(
92+
'{"event":"session_title_generated","sessionId":"sess-xyz","title":"Test Title"}\n',
93+
)
94+
const after = Date.now()
95+
96+
expect(result.events).toHaveLength(1)
97+
expect(result.events[0]).toMatchObject({
98+
streamEventType: "session_title_generated",
99+
sessionId: "sess-xyz",
100+
title: "Test Title",
101+
})
102+
const event = result.events[0] as { timestamp: number }
103+
expect(event.timestamp).toBeGreaterThanOrEqual(before)
104+
expect(event.timestamp).toBeLessThanOrEqual(after)
105+
})
106+
76107
it("should parse welcome event with worktree branch", () => {
77108
const result = parseCliChunk(
78109
'{"type":"welcome","metadata":{"welcomeOptions":{"worktreeBranch":"feature/test-branch"}},"timestamp":1234567890}\n',

src/shared/kilocode/cli-sessions/core/SessionManager.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ import { GitStateService } from "./GitStateService.js"
1919
import { SessionStateManager } from "./SessionStateManager.js"
2020
import { SyncQueue } from "./SyncQueue.js"
2121
import { TokenValidationService } from "./TokenValidationService.js"
22-
import { SessionTitleService } from "./SessionTitleService.js"
22+
import { SessionTitleService, type SessionTitleGeneratedMessage } from "./SessionTitleService.js"
2323
import { SessionLifecycleService } from "./SessionLifecycleService.js"
24-
import { SessionSyncService, type SessionCreatedMessage, type SessionSyncedMessage } from "./SessionSyncService.js"
24+
import {
25+
SessionSyncService,
26+
type SessionCreatedMessage,
27+
type SessionSyncedMessage,
28+
} from "./SessionSyncService.js"
2529
import { LOG_SOURCES } from "../config.js"
2630

2731
// Re-export types for external consumers
2832
export type { SessionCreatedMessage, SessionSyncedMessage } from "./SessionSyncService.js"
33+
export type { SessionTitleGeneratedMessage } from "./SessionTitleService.js"
2934
export type {
3035
ListSessionsInput,
3136
ListSessionsOutput,
@@ -45,6 +50,7 @@ export interface SessionManagerDependencies extends TrpcClientDependencies {
4550
onSessionCreated: (message: SessionCreatedMessage) => void
4651
onSessionRestored: () => void
4752
onSessionSynced: (message: SessionSyncedMessage) => void
53+
onSessionTitleGenerated: (message: SessionTitleGeneratedMessage) => void
4854
getOrganizationId: (taskId: string) => Promise<string | undefined>
4955
getMode: (taskId: string) => Promise<string | undefined>
5056
getModel: (taskId: string) => Promise<string | undefined>
@@ -126,6 +132,7 @@ export class SessionManager {
126132
stateManager: this.stateManager,
127133
extensionMessenger: dependencies.extensionMessenger,
128134
logger: this.logger,
135+
onSessionTitleGenerated: dependencies.onSessionTitleGenerated,
129136
})
130137
this.gitStateService = new GitStateService({
131138
logger: this.logger,

src/shared/kilocode/cli-sessions/core/SessionTitleService.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import type { ILogger } from "../types/ILogger.js"
55
import type { SessionClient } from "./SessionClient.js"
66
import type { SessionStateManager } from "./SessionStateManager.js"
77

8+
/**
9+
* Message emitted when a session title has been generated and updated.
10+
*/
11+
export interface SessionTitleGeneratedMessage {
12+
sessionId: string
13+
title: string
14+
timestamp: number
15+
event: "session_title_generated"
16+
}
17+
818
/**
919
* Dependencies required by SessionTitleService.
1020
*/
@@ -13,6 +23,7 @@ export interface SessionTitleServiceDependencies {
1323
stateManager: SessionStateManager
1424
extensionMessenger: IExtensionMessenger
1525
logger: ILogger
26+
onSessionTitleGenerated?: (message: SessionTitleGeneratedMessage) => void
1627
}
1728

1829
/**
@@ -35,6 +46,7 @@ export class SessionTitleService {
3546
private readonly stateManager: SessionStateManager
3647
private readonly extensionMessenger: IExtensionMessenger
3748
private readonly logger: ILogger
49+
private readonly onSessionTitleGenerated: (message: SessionTitleGeneratedMessage) => void
3850

3951
/**
4052
* Creates a new SessionTitleService instance.
@@ -55,6 +67,7 @@ export class SessionTitleService {
5567
this.stateManager = dependencies.stateManager
5668
this.extensionMessenger = dependencies.extensionMessenger
5769
this.logger = dependencies.logger
70+
this.onSessionTitleGenerated = dependencies.onSessionTitleGenerated ?? (() => {})
5871

5972
this.maxTitleLength = config.maxLength ?? DEFAULT_CONFIG.title.maxLength
6073
this.truncatedTitleLength = config.truncatedLength ?? DEFAULT_CONFIG.title.truncatedLength
@@ -169,6 +182,14 @@ Summary:`
169182
sessionId,
170183
title: trimmedTitle,
171184
})
185+
186+
// Emit session_title_generated event
187+
this.onSessionTitleGenerated({
188+
sessionId,
189+
title: trimmedTitle,
190+
timestamp: Date.now(),
191+
event: "session_title_generated",
192+
})
172193
}
173194

174195
/**

src/shared/kilocode/cli-sessions/core/__tests__/SessionManager.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe("SessionManager", () => {
4343
let mockOnSessionCreated: any
4444
let mockOnSessionRestored: any
4545
let mockOnSessionSynced: any
46+
let mockOnSessionTitleGenerated: any
4647
let mockGetOrganizationId: any
4748
let mockGetMode: any
4849
let mockGetModel: any
@@ -127,6 +128,7 @@ describe("SessionManager", () => {
127128
mockOnSessionCreated = vi.fn()
128129
mockOnSessionRestored = vi.fn()
129130
mockOnSessionSynced = vi.fn()
131+
mockOnSessionTitleGenerated = vi.fn()
130132
mockGetOrganizationId = vi.fn().mockResolvedValue("org-123")
131133
mockGetMode = vi.fn().mockResolvedValue("code")
132134
mockGetModel = vi.fn().mockResolvedValue("gpt-4")
@@ -144,6 +146,7 @@ describe("SessionManager", () => {
144146
onSessionCreated: mockOnSessionCreated,
145147
onSessionRestored: mockOnSessionRestored,
146148
onSessionSynced: mockOnSessionSynced,
149+
onSessionTitleGenerated: mockOnSessionTitleGenerated,
147150
getOrganizationId: mockGetOrganizationId,
148151
getMode: mockGetMode,
149152
getModel: mockGetModel,
@@ -177,6 +180,7 @@ describe("SessionManager", () => {
177180
onSessionCreated: mockOnSessionCreated,
178181
onSessionRestored: mockOnSessionRestored,
179182
onSessionSynced: mockOnSessionSynced,
183+
onSessionTitleGenerated: mockOnSessionTitleGenerated,
180184
getOrganizationId: mockGetOrganizationId,
181185
getMode: mockGetMode,
182186
getModel: mockGetModel,
@@ -208,6 +212,7 @@ describe("SessionManager", () => {
208212
onSessionCreated: mockOnSessionCreated,
209213
onSessionRestored: mockOnSessionRestored,
210214
onSessionSynced: mockOnSessionSynced,
215+
onSessionTitleGenerated: mockOnSessionTitleGenerated,
211216
getOrganizationId: mockGetOrganizationId,
212217
getMode: mockGetMode,
213218
getModel: mockGetModel,
@@ -259,6 +264,7 @@ describe("SessionManager", () => {
259264
onSessionCreated: mockOnSessionCreated,
260265
onSessionRestored: mockOnSessionRestored,
261266
onSessionSynced: mockOnSessionSynced,
267+
onSessionTitleGenerated: mockOnSessionTitleGenerated,
262268
getOrganizationId: mockGetOrganizationId,
263269
getMode: mockGetMode,
264270
getModel: mockGetModel,
@@ -373,6 +379,7 @@ describe("SessionManager", () => {
373379
onSessionCreated: mockOnSessionCreated,
374380
onSessionRestored: mockOnSessionRestored,
375381
onSessionSynced: mockOnSessionSynced,
382+
onSessionTitleGenerated: mockOnSessionTitleGenerated,
376383
getOrganizationId: mockGetOrganizationId,
377384
getMode: mockGetMode,
378385
getModel: mockGetModel,
@@ -402,6 +409,7 @@ describe("SessionManager", () => {
402409
onSessionCreated: mockOnSessionCreated,
403410
onSessionRestored: mockOnSessionRestored,
404411
onSessionSynced: mockOnSessionSynced,
412+
onSessionTitleGenerated: mockOnSessionTitleGenerated,
405413
getOrganizationId: mockGetOrganizationId,
406414
getMode: mockGetMode,
407415
getModel: mockGetModel,
@@ -423,6 +431,7 @@ describe("SessionManager", () => {
423431
onSessionCreated: mockOnSessionCreated,
424432
onSessionRestored: mockOnSessionRestored,
425433
onSessionSynced: mockOnSessionSynced,
434+
onSessionTitleGenerated: mockOnSessionTitleGenerated,
426435
getOrganizationId: mockGetOrganizationId,
427436
getMode: mockGetMode,
428437
getModel: mockGetModel,

src/shared/kilocode/cli-sessions/core/__tests__/SessionTitleService.spec.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,18 +215,38 @@ describe("SessionTitleService", () => {
215215

216216
it("updates state manager with timestamp", async () => {
217217
await service.updateTitle("session-123", "Test title")
218-
218+
219219
expect(mockStateManager.updateTimestamp).toHaveBeenCalledWith("session-123", "2023-01-01T10:00:00Z")
220220
})
221-
221+
222222
it("logs success message", async () => {
223223
await service.updateTitle("session-123", "Test title")
224-
224+
225225
expect(mockLogger.info).toHaveBeenCalledWith("Session title updated successfully", "SessionTitleService", {
226226
sessionId: "session-123",
227227
title: "Test title",
228228
})
229229
})
230+
231+
it("emits session_title_generated event", async () => {
232+
const onSessionTitleGenerated = vi.fn()
233+
const serviceWithCallback = new SessionTitleService({
234+
sessionClient: mockSessionClient as any,
235+
stateManager: mockStateManager as any,
236+
extensionMessenger: mockExtensionMessenger as any,
237+
logger: mockLogger as any,
238+
onSessionTitleGenerated,
239+
})
240+
241+
await serviceWithCallback.updateTitle("session-123", "Test title")
242+
243+
expect(onSessionTitleGenerated).toHaveBeenCalledWith({
244+
sessionId: "session-123",
245+
title: "Test title",
246+
timestamp: expect.any(Number),
247+
event: "session_title_generated",
248+
})
249+
})
230250
})
231251

232252
describe("generateAndUpdateTitle", () => {

src/shared/kilocode/cli-sessions/extension/session-manager-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export function kilo_initializeSessionManager({
5858
onSessionSynced: (message) => {
5959
log(`Session synced: ${message.sessionId}`)
6060
},
61+
onSessionTitleGenerated: (message) => {
62+
log(`Session title generated: ${message.sessionId} - ${message.title}`)
63+
},
6164
platform: vscode.env.appName,
6265
getOrganizationId: async (taskId: string) => {
6366
const result = await (async () => {

0 commit comments

Comments
 (0)