Add chat session title to View Chat Terminals action (#274828)

This commit is contained in:
Copilot
2025-11-03 21:02:20 +00:00
committed by GitHub
parent b8b8231b67
commit d548d94db6
4 changed files with 82 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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<ITerminalInstance>([...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

View File

@@ -26,7 +26,10 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ
declare _serviceBrand: undefined;
private readonly _terminalInstancesByToolSessionId = new Map<string, ITerminalInstance>();
private readonly _toolSessionIdByTerminalInstance = new Map<ITerminalInstance, string>();
private readonly _chatSessionIdByTerminalInstance = new Map<ITerminalInstance, string>();
private readonly _terminalInstanceListenersByToolSessionId = this._register(new DisposableMap<string, IDisposable>());
private readonly _chatSessionListenersByTerminalInstance = this._register(new DisposableMap<ITerminalInstance, IDisposable>());
private readonly _onDidRegisterTerminalInstanceForToolSession = new Emitter<ITerminalInstance>();
readonly onDidRegisterTerminalInstanceWithToolSession: Event<ITerminalInstance> = 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();
}));

View File

@@ -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(() => {