From d548d94db656d31f6142fa69ef5b27c236849e62 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:02:20 +0000 Subject: [PATCH] Add chat session title to View Chat Terminals action (#274828) --- .../contrib/terminal/browser/terminal.ts | 22 ++++++++++++ .../chat/browser/terminalChatActions.ts | 24 ++++++++++++- .../chat/browser/terminalChatService.ts | 34 +++++++++++++++++++ .../browser/tools/runInTerminalTool.ts | 3 ++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 6ea2688d038..5372e518f44 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -134,6 +134,28 @@ export interface ITerminalChatService { */ getToolSessionTerminalInstances(): readonly ITerminalInstance[]; + /** + * Returns the tool session ID for a given terminal instance, if it has been registered. + * @param instance The terminal instance to look up + * @returns The tool session ID if found, undefined otherwise + */ + getToolSessionIdForInstance(instance: ITerminalInstance): string | undefined; + + /** + * Associate a chat session ID with a terminal instance. This is used to retrieve the chat + * session title for display purposes. + * @param chatSessionId The chat session ID + * @param instance The terminal instance + */ + registerTerminalInstanceWithChatSession(chatSessionId: string, instance: ITerminalInstance): void; + + /** + * Returns the chat session ID for a given terminal instance, if it has been registered. + * @param instance The terminal instance to look up + * @returns The chat session ID if found, undefined otherwise + */ + getChatSessionIdForInstance(instance: ITerminalInstance): string | undefined; + isBackgroundTerminal(terminalToolSessionId?: string): boolean; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index e1c9dc9c471..ce640692096 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -12,6 +12,7 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { ChatViewId, IChatWidgetService } from '../../../chat/browser/chat.js'; import { ChatContextKeys } from '../../../chat/common/chatContextKeys.js'; import { IChatService } from '../../../chat/common/chatService.js'; +import { LocalChatSessionUri } from '../../../chat/common/chatUri.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { AbstractInline1ChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; import { isDetachedTerminalInstance, ITerminalChatService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; @@ -327,6 +328,7 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { const terminalChatService = accessor.get(ITerminalChatService); const quickInputService = accessor.get(IQuickInputService); const instantiationService = accessor.get(IInstantiationService); + const chatService = accessor.get(IChatService); const visible = new Set([...groupService.instances, ...editorService.instances]); const toolInstances = terminalChatService.getToolSessionTerminalInstances(); @@ -357,9 +359,29 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { const iconId = instantiationService.invokeFunction(getIconId, instance); const label = `$(${iconId}) ${instance.title}`; const lastCommand = instance.capabilities.get(TerminalCapability.CommandDetection)?.commands.at(-1)?.command; + + // Get the chat session title + const chatSessionId = terminalChatService.getChatSessionIdForInstance(instance); + let chatSessionTitle: string | undefined; + if (chatSessionId) { + const sessionUri = LocalChatSessionUri.forSession(chatSessionId); + // Try to get title from active session first, then fall back to persisted title + chatSessionTitle = chatService.getSession(sessionUri)?.title || chatService.getPersistedSessionTitle(sessionUri); + } + + // Build description: chat session title and/or hidden status + let description: string | undefined; + if (chatSessionTitle && isBackground) { + description = `${chatSessionTitle} • ${hiddenLocalized}`; + } else if (chatSessionTitle) { + description = chatSessionTitle; + } else if (isBackground) { + description = hiddenLocalized; + } + metas.push({ label, - description: isBackground ? hiddenLocalized : undefined, + description, detail: lastCommand ? lastCommandLocalized(lastCommand) : undefined, id: String(instance.instanceId), isBackground diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts index c9de0afee76..b629add0cf1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts @@ -26,7 +26,10 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ declare _serviceBrand: undefined; private readonly _terminalInstancesByToolSessionId = new Map(); + private readonly _toolSessionIdByTerminalInstance = new Map(); + private readonly _chatSessionIdByTerminalInstance = new Map(); private readonly _terminalInstanceListenersByToolSessionId = this._register(new DisposableMap()); + private readonly _chatSessionListenersByTerminalInstance = this._register(new DisposableMap()); private readonly _onDidRegisterTerminalInstanceForToolSession = new Emitter(); readonly onDidRegisterTerminalInstanceWithToolSession: Event = this._onDidRegisterTerminalInstanceForToolSession.event; @@ -61,9 +64,11 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ return; } this._terminalInstancesByToolSessionId.set(terminalToolSessionId, instance); + this._toolSessionIdByTerminalInstance.set(instance, terminalToolSessionId); this._onDidRegisterTerminalInstanceForToolSession.fire(instance); this._terminalInstanceListenersByToolSessionId.set(terminalToolSessionId, instance.onDisposed(() => { this._terminalInstancesByToolSessionId.delete(terminalToolSessionId); + this._toolSessionIdByTerminalInstance.delete(instance); this._terminalInstanceListenersByToolSessionId.deleteAndDispose(terminalToolSessionId); this._persistToStorage(); this._updateHasToolTerminalContextKeys(); @@ -72,6 +77,7 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._register(this._chatService.onDidDisposeSession(e => { if (LocalChatSessionUri.parseLocalSessionId(e.sessionResource) === terminalToolSessionId) { this._terminalInstancesByToolSessionId.delete(terminalToolSessionId); + this._toolSessionIdByTerminalInstance.delete(instance); this._terminalInstanceListenersByToolSessionId.deleteAndDispose(terminalToolSessionId); this._persistToStorage(); this._updateHasToolTerminalContextKeys(); @@ -108,6 +114,32 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ return Array.from(new Set(this._terminalInstancesByToolSessionId.values())); } + getToolSessionIdForInstance(instance: ITerminalInstance): string | undefined { + return this._toolSessionIdByTerminalInstance.get(instance); + } + + registerTerminalInstanceWithChatSession(chatSessionId: string, instance: ITerminalInstance): void { + // If already registered with the same session ID, skip to avoid duplicate listeners + if (this._chatSessionIdByTerminalInstance.get(instance) === chatSessionId) { + return; + } + + // Clean up previous listener if the instance was registered with a different session + this._chatSessionListenersByTerminalInstance.deleteAndDispose(instance); + + this._chatSessionIdByTerminalInstance.set(instance, chatSessionId); + // Clean up when the instance is disposed + const disposable = instance.onDisposed(() => { + this._chatSessionIdByTerminalInstance.delete(instance); + this._chatSessionListenersByTerminalInstance.deleteAndDispose(instance); + }); + this._chatSessionListenersByTerminalInstance.set(instance, disposable); + } + + getChatSessionIdForInstance(instance: ITerminalInstance): string | undefined { + return this._chatSessionIdByTerminalInstance.get(instance); + } + isBackgroundTerminal(terminalToolSessionId?: string): boolean { if (!terminalToolSessionId) { return false; @@ -144,9 +176,11 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ for (const [toolSessionId, persistentProcessId] of this._pendingRestoredMappings) { if (persistentProcessId === instance.shellLaunchConfig.attachPersistentProcess?.id) { this._terminalInstancesByToolSessionId.set(toolSessionId, instance); + this._toolSessionIdByTerminalInstance.set(instance, toolSessionId); this._onDidRegisterTerminalInstanceForToolSession.fire(instance); this._terminalInstanceListenersByToolSessionId.set(toolSessionId, instance.onDisposed(() => { this._terminalInstancesByToolSessionId.delete(toolSessionId); + this._toolSessionIdByTerminalInstance.delete(instance); this._terminalInstanceListenersByToolSessionId.deleteAndDispose(toolSessionId); this._persistToStorage(); })); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 29f72a8e45a..039e4ebf8bc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -705,6 +705,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const profile = await this._profileFetcher.getCopilotProfile(); const toolTerminal = await this._terminalToolCreator.createTerminal(profile, token); this._terminalChatService.registerTerminalInstanceWithToolSession(terminalToolSessionId, toolTerminal.instance); + this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionId, toolTerminal.instance); this._registerInputListener(toolTerminal); this._sessionTerminalAssociations.set(chatSessionId, toolTerminal); if (token.isCancellationRequested) { @@ -726,6 +727,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const profile = await this._profileFetcher.getCopilotProfile(); const toolTerminal = await this._terminalToolCreator.createTerminal(profile, token); this._terminalChatService.registerTerminalInstanceWithToolSession(terminalToolSessionId, toolTerminal.instance); + this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionId, toolTerminal.instance); this._registerInputListener(toolTerminal); this._sessionTerminalAssociations.set(chatSessionId, toolTerminal); if (token.isCancellationRequested) { @@ -766,6 +768,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { shellIntegrationQuality: association.shellIntegrationQuality }; this._sessionTerminalAssociations.set(association.sessionId, toolTerminal); + this._terminalChatService.registerTerminalInstanceWithChatSession(association.sessionId, instance); // Listen for terminal disposal to clean up storage this._register(instance.onDisposed(() => {