src/vs/platform/agentHost/node/agentHostSessionTitleController.ts

507 LOC · 473 covered · 34 uncovered · 124 ranges · 1112 concepts · 56 introducers · 521 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentHostSessionTitleController.ts ×25
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { ILogService } from '../../log/common/log.js';
10 > import { ISessionDataService } from '../common/sessionDataService.js';
11 > import { ActionType } from '../common/state/sessionActions.js';
12 > import { isAhpChatChannel, isDefaultChatUri, type Turn, type URI as ProtocolURI } from '../common/state/sessionState.js';
13 > import { buildConversationContext, renderResponseMarkdown, truncateMiddle } from '../common/agentHostConversationContext.js';
14 > import { AgentHostStateManager } from './agentHostStateManager.js';
15 > import { ICopilotApiService, type ICopilotUtilityChatMessage } from './shared/copilotApiService.js';
16 >
17 > const MAX_TITLE_LENGTH = 200;
18 >
19 > /**
20 > * Soft upper bound, in characters, for the first-turn context fed to the
21 > * utility model when refining a session title. Sized to stay well within the
22 > * small model's context window while leaving room for the prompt scaffolding.
23 > */
24 > const MAX_TITLE_CONTEXT_CHARS = 20000;
25 >
26 > export interface IAgentHostSessionTitleControllerOptions {
27 > readonly sessionDataService: ISessionDataService;
28 > readonly getGitHubCopilotToken?: () => string | undefined;
29 > readonly copilotApiService?: ICopilotApiService;
30 > }
31 >
32 > export class AgentHostSessionTitleController extends Disposable {
33 >
34 > private readonly _titleGenerationCancellationSources = new Map<ProtocolURI, CancellationTokenSource>();
35 >
36 > /**
37 > * The most recent title this controller applied for a given session/chat
38 > * key. Used to detect whether the title was changed (e.g. a manual
39 > * `/rename` or user edit) since we last set it, so we never clobber a
40 > * deliberate title with an auto-generated one.
41 > */
42 > private readonly _lastAppliedTitle = new Map<ProtocolURI, string>();
43 >
44 > /**
45 > * Session/chat keys whose current title is a provisional placeholder set by
46 > * {@link seedProvisionalTitle} (e.g. from a `!command`). Such a title does
47 > * not describe the session's topic, so the first subsequent request that
48 > * carries real intent replaces it with a generated title via
49 > * {@link seedTitleFromFirstMessage}.
50 > */
51 > private readonly _provisionalTitles = new Set<ProtocolURI>();
52 >
53 > constructor(
54 > private readonly _stateManager: AgentHostStateManager, agentHostSessionTitleController.ts ×3
55 > private readonly _options: IAgentHostSessionTitleControllerOptions,
56 > @ILogService private readonly _logService: ILogService,
57 > ) {
58 > super();
59 > }
61 > seedTitleFromFirstMessage(channel: ProtocolURI, userPrompt: string, chatChannel?: ProtocolURI): void {
62 > const fallbackTitle = this._normalizeTitle(userPrompt); agentHostSessionTitleController.ts ×3
63 > if (!fallbackTitle) {
65 > }
67 > const additionalChat = this._additionalChatChannel(chatChannel);
68 > const key = additionalChat ?? channel;
69 > const state = additionalChat ? this._stateManager.getChatState(additionalChat) : this._stateManager.getSessionState(channel); agentHostSessionTitleController.ts ×3
70 > if (!state || !this._canSeedFirstMessageTitle(key, state.turns.length, state.title)) {
72 > }
73 > const replacesProvisionalTitle = this._provisionalTitles.has(key); agentHostSessionTitleController.ts ×2
74 > this._provisionalTitles.delete(key);
75 > this._applySeedTitle(channel, additionalChat, fallbackTitle);
76 > if (replacesProvisionalTitle) {
77 > this._persistSeedTitle(channel, additionalChat, fallbackTitle); agentHostSessionTitleController.ts ×1
78 > }
79 > this._generateTitleSoon( agentHostSessionTitleController.ts ×2
80 > key,
81 > userPrompt,
82 > false,
83 > fallbackTitle,
84 > title => this._applySeedTitle(channel, additionalChat, title),
85 > () => this._currentSeedTitle(channel, additionalChat) === this._lastAppliedTitle.get(key),
86 > title => this._persistSeedTitle(channel, additionalChat, title),
87 > );
90 > /** Seeds and persists a provisional title suggested by a locally handled command. */
91 > seedProvisionalTitle(channel: ProtocolURI, suggestedTitle: string, chatChannel?: ProtocolURI): void {
92 > const title = this._normalizeTitle(suggestedTitle); agentHostSessionTitleController.ts ×3
93 > if (!title) {
95 > }
97 > const additionalChat = this._additionalChatChannel(chatChannel);
98 > const key = additionalChat ?? channel;
99 > const state = additionalChat ? this._stateManager.getChatState(additionalChat) : this._stateManager.getSessionState(channel); agentHostSessionTitleController.ts ×3
100 > if (!state || !this._canSeedProvisionalTitle(key, state.title)) {
102 > }
103 > this._provisionalTitles.add(key); agentHostSessionTitleController.ts ×2
104 > this._applySeedTitle(channel, additionalChat, title);
105 > this._persistSeedTitle(channel, additionalChat, title);
108 > /** Trims, collapses whitespace, and length-caps a candidate title. */
109 > private _normalizeTitle(text: string): string {
110 > return text.trim().replace(/\s+/g, ' ').slice(0, MAX_TITLE_LENGTH); agentHostSessionTitleController.ts ×1
111 > }
113 > /**
114 > * The peer (additional) chat a seed should title, or `undefined` to title
115 > * the session itself. The default chat maps to the session.
116 > */
117 > private _additionalChatChannel(chatChannel?: ProtocolURI): ProtocolURI | undefined {
118 > return !!chatChannel && isAhpChatChannel(chatChannel) && !isDefaultChatUri(chatChannel) ? chatChannel : undefined; agentHostSessionTitleController.ts ×1
119 > }
121 > /**
122 > * Applies `title` to the addressed peer chat (`additionalChat`) or, when
123 > * that is `undefined`, to the session itself, recording it as last-applied.
124 > */
125 > private _applySeedTitle(channel: ProtocolURI, additionalChat: ProtocolURI | undefined, title: string): void {
126 > if (additionalChat) { agentHostSessionTitleController.ts ×3
127 > this._applyTitle(additionalChat, title, t => this._stateManager.updateChatTitle(channel, additionalChat, t)); agentHostSessionTitleController.ts ×1
129 > this._applyTitle(channel, title, t => this._stateManager.dispatchServerAction(channel, { agentHostSessionTitleController.ts ×1
130 > type: ActionType.SessionTitleChanged,
131 > title: t,
132 > }));
133 > }
136 > /** Persists `title` as the custom title of the addressed peer chat or session. */
137 > private _persistSeedTitle(channel: ProtocolURI, additionalChat: ProtocolURI | undefined, title: string): void {
138 > this._persistSessionFlag(channel, additionalChat ? `customChatTitle:${additionalChat}` : 'customTitle', title); agentHostSessionTitleController.ts ×1
139 > }
141 > /** The live title of the addressed peer chat or session. */
142 > private _currentSeedTitle(channel: ProtocolURI, additionalChat: ProtocolURI | undefined): string | undefined {
143 > return additionalChat ? this._stateManager.getChatState(additionalChat)?.title : this._stateManager.getSessionState(channel)?.title; agentHostSessionTitleController.ts ×1
144 > }
146 > /**
147 > * Whether {@link seedTitleFromFirstMessage} may (re)title `key`: true for a
148 > * fresh, untitled target (its first message) or when its title is a
149 > * provisional placeholder we applied and no one has changed it since — the
150 > * first real request supersedes the placeholder.
151 > */
152 > private _canSeedFirstMessageTitle(key: ProtocolURI, turnsLength: number, currentTitle: string | undefined): boolean {
153 > if (turnsLength === 0 && !currentTitle) { agentHostSessionTitleController.ts ×2
155 > }
156 > return this._provisionalTitles.has(key) && !!currentTitle && currentTitle === this._lastAppliedTitle.get(key); agentHostSessionTitleController.ts ×2
157 > }
159 > /**
160 > * Whether {@link seedProvisionalTitle} may (re)title `key`: true when it is
161 > * untitled (the first message carried a suggestion) or when its title is a
162 > * provisional placeholder we applied and no one has changed it since —
163 > * successive suggestions keep the newest one visible without clobbering a
164 > * manual rename.
165 > */
166 > private _canSeedProvisionalTitle(key: ProtocolURI, currentTitle: string | undefined): boolean {
167 > if (!currentTitle) { agentHostSessionTitleController.ts ×3
169 > }
170 > return this._provisionalTitles.has(key) && currentTitle === this._lastAppliedTitle.get(key); agentHostSessionTitleController.ts ×1
173 > /**
174 > * Re-generates the title once the first turn has completed, this time
175 > * using the full first-turn context (the user request plus the agent's
176 > * textual response) rather than just the opening message. This only runs
177 > * for the very first turn and only when the current title is still the one
178 > * this controller last applied — a manual `/rename`, a user edit, or a
179 > * forked session's inherited title all suppress it.
180 > *
181 > * Only normal text response parts are considered (tool calls, reasoning,
182 > * and other parts are ignored). If the context still exceeds the budget
183 > * the middle is removed (marked with `...`). The user's first request is
184 > * always preserved.
185 > */
186 > refineTitleFromFirstTurn(channel: ProtocolURI, chatChannel?: ProtocolURI): void {
187 > const isAdditionalChat = !!chatChannel && isAhpChatChannel(chatChannel) && !isDefaultChatUri(chatChannel); agentHostSessionTitleController.ts ×4
188 > if (isAdditionalChat) {
189 > const chatState = this._stateManager.getChatState(chatChannel); agentHostSessionTitleController.ts ×2
190 > if (!chatState || chatState.turns.length !== 1) {
191 return;
192 }
193 > const lastApplied = this._lastAppliedTitle.get(chatChannel); agentHostSessionTitleController.ts ×2
194 > if (lastApplied === undefined || chatState.title !== lastApplied) {
195 > return;
196 > }
197 const context = this._buildFirstTurnContext(chatState.turns[0]);
198 if (!context) {
199 return;
200 }
201 const apply = (title: string) => this._applyTitle(chatChannel, title, t => this._stateManager.updateChatTitle(channel, chatChannel, t));
202 this._generateTitleSoon(
203 chatChannel,
204 context,
205 true,
206 lastApplied,
207 apply,
208 () => this._stateManager.getChatState(chatChannel)?.title === this._lastAppliedTitle.get(chatChannel),
209 title => this._persistSessionFlag(channel, `customChatTitle:${chatChannel}`, title),
210 );
211 return;
212 }
214 > const state = this._stateManager.getSessionState(channel);
215 > if (!state || state.turns.length !== 1) { agentHostSessionTitleController.ts ×4
217 > }
218 > const lastApplied = this._lastAppliedTitle.get(channel); agentHostSessionTitleController.ts ×1
219 > if (lastApplied === undefined || state.title !== lastApplied) { agentHostSessionTitleController.ts ×4
221 > }
222 > const context = this._buildFirstTurnContext(state.turns[0]); agentHostSessionTitleController.ts ×3
223 > if (!context) {
225 > }
226 > const apply = (title: string) => this._applyTitle(channel, title, t => this._stateManager.dispatchServerAction(channel, { agentHostSessionTitleController.ts ×3
227 > type: ActionType.SessionTitleChanged,
228 > title: t,
229 > }));
230 > this._generateTitleSoon(
231 > channel,
232 > context,
233 > true,
234 > lastApplied,
235 > apply,
236 > () => this._stateManager.getSessionState(channel)?.title === this._lastAppliedTitle.get(channel),
237 > title => this._persistSessionFlag(channel, 'customTitle', title),
238 > );
241 > /**
242 > * Generates a title for a freshly forked session or chat from its
243 > * inherited conversation context. Forks copy the source history up to the
244 > * fork point, so neither {@link seedTitleFromFirstMessage} nor
245 > * {@link refineTitleFromFirstTurn} (which require an empty / single-turn
246 > * state) ever fire for them. This is the fork equivalent, run once at fork
247 > * time over the kept turns, so the new chat gets a content-derived title
248 > * instead of permanently inheriting the source's `Forked: …` title.
249 > *
250 > * `fallbackTitle` is the title the caller already applied to the new
251 > * session/chat (e.g. `Forked: <source>`); it is recorded as the
252 > * last-applied title so a concurrent manual rename suppresses the
253 > * generated title, and stays visible until generation completes. The
254 > * context is bounded to {@link MAX_TITLE_CONTEXT_CHARS} (middle-truncated),
255 > * so generation costs at most a single small-model call.
256 > */
257 > generateForkedTitle(channel: ProtocolURI, chatChannel: ProtocolURI | undefined, turns: readonly Turn[], fallbackTitle: string, sourceTitle?: string): void {
258 > const context = this._buildConversationContext(turns, sourceTitle); agentHostSessionTitleController.ts ×5
259 > if (!context) {
260 return;
261 }
263 > const isAdditionalChat = !!chatChannel && isAhpChatChannel(chatChannel) && !isDefaultChatUri(chatChannel);
264 > if (isAdditionalChat) {
265 > const key = chatChannel; agentService.ts ×4
266 > this._lastAppliedTitle.set(key, fallbackTitle);
267 > const apply = (title: string) => this._applyTitle(key, title, t => this._stateManager.updateChatTitle(channel, key, t));
268 > this._generateTitleSoon(
269 > key,
270 > context,
271 > true,
272 > fallbackTitle,
273 > apply,
274 > () => this._stateManager.getChatState(key)?.title === this._lastAppliedTitle.get(key),
275 > title => this._persistSessionFlag(channel, `customChatTitle:${key}`, title),
276 > );
277 > return;
278 > }
280 > this._lastAppliedTitle.set(channel, fallbackTitle);
281 > const apply = (title: string) => this._applyTitle(channel, title, t => this._stateManager.dispatchServerAction(channel, {
282 > type: ActionType.SessionTitleChanged, agentHostSessionTitleController.ts ×1
283 > title: t,
284 > }));
285 > this._generateTitleSoon( agentHostSessionTitleController.ts ×2
286 > channel,
287 > context,
288 > true,
289 > fallbackTitle,
290 > apply,
291 > () => this._stateManager.getSessionState(channel)?.title === this._lastAppliedTitle.get(channel),
292 > title => this._persistSessionFlag(channel, 'customTitle', title),
293 > );
296 > private _applyTitle(key: ProtocolURI, title: string, dispatch: (title: string) => void): void {
297 > this._lastAppliedTitle.set(key, title); agentHostSessionTitleController.ts ×1
298 > dispatch(title);
299 > }
301 > cancelTitleGeneration(session: ProtocolURI): void {
302 > this._cancelTitleGeneration(session); agentHostSessionTitleController.ts ×1
303 > }
305 > private _generateTitleSoon(
306 > key: ProtocolURI, agentHostSessionTitleController.ts ×8
307 > promptContent: string,
308 > isConversation: boolean,
309 > fallbackTitle: string,
310 > apply: (title: string) => void,
311 > currentTitleMatchesFallback: () => boolean,
312 > persist: (title: string) => void,
313 > ): void {
314 > this._cancelTitleGeneration(key);
315 > const source = new CancellationTokenSource();
316 > this._titleGenerationCancellationSources.set(key, source);
317 > void this._generateTitle(key, promptContent, isConversation, fallbackTitle, apply, currentTitleMatchesFallback, persist, source.token).catch(err => {
318 if (!source.token.isCancellationRequested) {
319 this._logService.warn(`[AgentHostSessionTitleController] Failed to apply generated title for ${key}`, err);
320 }
321 > }).finally(() => { agentHostSessionTitleController.ts ×8
322 > if (this._titleGenerationCancellationSources.get(key) === source) {
323 > this._titleGenerationCancellationSources.delete(key); agentHostSessionTitleController.ts ×1
324 > source.dispose();
325 > }
327 > }
329 > private async _generateTitle(
330 > key: ProtocolURI, agentHostSessionTitleController.ts ×8
331 > promptContent: string,
332 > isConversation: boolean,
333 > fallbackTitle: string,
334 > apply: (title: string) => void,
335 > currentTitleMatchesFallback: () => boolean,
336 > persist: (title: string) => void,
337 > token: CancellationToken,
338 > ): Promise<void> {
339 > const generatedTitle = await this._generateTitleFromPrompt(promptContent, isConversation, token);
340 > if (token.isCancellationRequested || !generatedTitle) {
342 > }
344 > if (!currentTitleMatchesFallback()) {
346 > }
348 > if (generatedTitle !== fallbackTitle) {
349 > apply(generatedTitle);
350 > }
351 > persist(generatedTitle);
354 > private async _generateTitleFromPrompt(promptContent: string, isConversation: boolean, token: CancellationToken): Promise<string | undefined> {
355 > if (token.isCancellationRequested) { agentHostSessionTitleController.ts ×8
356 return undefined;
357 }
359 > const githubToken = this._options.getGitHubCopilotToken?.();
360 > const copilotApiService = this._options.copilotApiService;
361 > if (!githubToken || !copilotApiService) {
362 > return undefined; agentHostSessionTitleController.ts ×1
363 > }
365 > const abortController = new AbortController();
366 > const cancellationListener = token.onCancellationRequested(() => abortController.abort());
367 > try {
368 > const rawTitle = await copilotApiService.utilityChatCompletion(githubToken, {
369 > messages: this._buildTitlePrompt(promptContent, isConversation),
370 > }, {
371 > signal: abortController.signal,
372 > });
373 > return this._cleanTitle(rawTitle); agentHostSessionTitleController.ts ×4
375 > if (token.isCancellationRequested) { agentHostSessionTitleController.ts ×2
376 return undefined;
377 }
378 > this._logService.warn('[AgentHostSessionTitleController] Failed to generate session title', err); agentHostSessionTitleController.ts ×2
379 > return undefined;
381 > cancellationListener.dispose();
382 > }
385 > private _buildTitlePrompt(promptContent: string, isConversation: boolean): ICopilotUtilityChatMessage[] {
386 > const userInstruction = isConversation agentHostSessionTitleController.ts ×5
387 > ? `Please write a brief title for the following conversation:\n\n${promptContent}` agentHostSessionTitleController.ts ×1
388 > : `Please write a brief title for the following request:\n\n${promptContent}`; agentHostSessionTitleController.ts ×1
390 > {
391 > role: 'system',
392 > content: [
393 > 'You are an expert in crafting ultra-compact titles for chatbot conversations.',
394 > 'You are presented with a chat request or conversation, and you reply with only a brief title that captures the main topic.',
395 > 'Write the title in sentence case, not title case.',
396 > 'Preserve product names, abbreviations, code symbols, and proper nouns.',
397 > 'Aim for 3-6 words. Prefer the shortest accurate title.',
398 > 'Drop articles like "a", "an", and "the" unless needed for clarity.',
399 > 'Drop filler and generic framing like "help with", "question about", "request for", or "issue with".',
400 > 'Never describe the chat itself as forked, branched, or continued — title only the underlying topic.',
401 > 'Prefer short, concrete synonyms and omit unnecessary words.',
402 > 'Do not wrap the title in quotes or add trailing punctuation.',
403 > ].join(' '),
404 > },
405 > {
406 > role: 'user',
407 > content: userInstruction,
408 > },
409 > ];
410 > }
412 > private _cleanTitle(rawTitle: string): string | undefined {
413 > let title = rawTitle.trim(); agentHostSessionTitleController.ts ×4
414 > const firstLine = title.split(/\r?\n/).map(line => line.trim()).find(line => line.length > 0);
415 > title = firstLine ?? '';
416 > if (title.startsWith('"') && title.endsWith('"') && title.length > 1) {
417 > title = title.slice(1, -1).trim(); agentHostSessionTitleController.ts ×1
418 > }
419 > title = title.replace(/[.!?]+$/, '').trim(); agentHostSessionTitleController.ts ×4
420 >
421 > if (!title || title.includes('can\'t assist with that')) {
422 return undefined;
423 }
424 > return title.slice(0, MAX_TITLE_LENGTH); agentHostSessionTitleController.ts ×4
425 > }
427 > /**
428 > * Builds the first-turn context string for title refinement. The user's
429 > * request is always kept (truncated in the middle only if it alone exceeds
430 > * half the budget). Only normal text (markdown) response parts are
431 > * considered — tool calls, reasoning, and other parts are ignored. If the
432 > * combined text is over budget, the middle of the response is removed.
433 > *
434 > * @returns the context string, or `undefined` when the turn has no text
435 > * response worth refining from (the opening message already produced a
436 > * title in that case).
437 > */
438 > private _buildFirstTurnContext(turn: Turn): string | undefined {
439 > const response = renderResponseMarkdown(turn.responseParts); agentHostSessionTitleController.ts ×3
440 > if (!response) {
441 > return undefined; agentHostSessionTitleController.ts ×2
442 > }
444 > const userBudget = Math.floor(MAX_TITLE_CONTEXT_CHARS / 2);
445 > let userRequest = turn.message.text.trim();
446 > if (userRequest.length > userBudget) {
447 userRequest = truncateMiddle(userRequest, userBudget);
448 }
449 > const userBlock = `User request:\n${userRequest}`; agentHostSessionTitleController.ts ×3
450 > const responseLabel = '\n\nAgent response:\n';
451 >
452 > const responseBudget = Math.max(0, MAX_TITLE_CONTEXT_CHARS - userBlock.length - responseLabel.length);
453 > const trimmedResponse = response.length > responseBudget ? truncateMiddle(response, responseBudget) : response; agentHostSessionTitleController.ts ×3
454 >
455 > return trimmedResponse ? `${userBlock}${responseLabel}${trimmedResponse}` : userBlock;
456 > }
458 > /**
459 > * Builds a conversation context string for forked-title generation by
460 > * concatenating each kept turn's user request and textual response. Only
461 > * normal text (markdown) response parts are considered — tool calls,
462 > * reasoning, and other parts are ignored, mirroring
463 > * {@link _buildFirstTurnContext}. When the fork's `sourceTitle` is known, a
464 > * short framing note is prepended so the model understands the conversation
465 > * is a branch continued from an earlier chat. The conversation is
466 > * middle-truncated to {@link MAX_TITLE_CONTEXT_CHARS} to bound model cost;
467 > * the framing note is always preserved in full.
468 > *
469 > * @returns the context string, or `undefined` when no turn carries any
470 > * text worth titling from.
471 > */
472 > private _buildConversationContext(turns: readonly Turn[], sourceTitle?: string): string | undefined {
473 > const framedTitle = sourceTitle?.trim(); agentHostSessionTitleController.ts ×5
474 > const framing = framedTitle
475 > ? `This conversation was branched from an earlier chat titled "${framedTitle}". The turns below, oldest first, are the inherited history up to the branch point.\n\n` agentHostSessionTitleController.ts ×1
477 > return buildConversationContext(turns, { maxChars: MAX_TITLE_CONTEXT_CHARS, framing }); agentHostSessionTitleController.ts ×5
478 > }
480 > private _persistSessionFlag(session: ProtocolURI, key: string, value: string): void {
481 > const ref = this._options.sessionDataService.openDatabase(URI.parse(session)); agentHostSessionTitleController.ts ×2
482 > ref.object.setMetadata(key, value).catch(err => {
483 this._logService.warn(`[AgentHostSessionTitleController] Failed to persist ${key}`, err);
484 > }).finally(() => { agentHostSessionTitleController.ts ×2
485 > ref.dispose();
486 > });
487 > }
489 > private _cancelTitleGeneration(session: ProtocolURI): void {
490 > const source = this._titleGenerationCancellationSources.get(session); agentHostSessionTitleController.ts ×2
491 > if (!source) {
492 > return;
493 > }
494 > source.dispose(true); agentHostSessionTitleController.ts ×1
495 > this._titleGenerationCancellationSources.delete(session);
498 > override dispose(): void {
499 > for (const source of this._titleGenerationCancellationSources.values()) { agentHostSessionTitleController.ts ×3
500 source.dispose(true);
501 }
502 > this._titleGenerationCancellationSources.clear(); agentHostSessionTitleController.ts ×3
503 > this._lastAppliedTitle.clear();
504 > this._provisionalTitles.clear();
505 > super.dispose();
506 > }