From a80c38a6763ea4c95c2e6ba365fbb236463c1882 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 20 Sep 2026 19:11:10 +0200 Subject: [PATCH 1/2] sessions: show F2 for chat rename actions Route header and nested-chat rename menus through their F2-bound actions while preserving inline rename behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/sessionsChatAccessibilityHelp.ts | 2 +- .../sessions/browser/sessionsActions.ts | 42 ++++++++----------- .../sessions/browser/views/sessionsList.ts | 5 +-- .../browser/sessionsListContextMenu.test.ts | 17 ++++---- .../test/browser/sessionsRename.test.ts | 10 ++++- .../sessionsSessionManagementActions.test.ts | 10 +++++ 6 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts index c173dd89a253e..299a266740acd 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts @@ -113,7 +113,7 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat content.push(localize('sessionsChat.goForward', "Go forward through visited sessions{0}.", '')); content.push(localize('sessionsChat.navigatePreviousSession', "Navigate to the previous session in the list{0}.", '')); content.push(localize('sessionsChat.navigateNextSession', "Navigate to the next session in the list{0}.", '')); - content.push(localize('sessionsChat.renameSession', "To rename a session inline, focus its row in the Sessions list and invoke Rename{0}, double-click its title, or open its context menu and choose Rename. Type the new title, then press Enter to confirm or Escape to cancel. From the main chat transcript or input, invoking Rename opens a prompt instead.", ``)); + content.push(localize('sessionsChat.renameSession', "To rename a session inline, focus its row in the Sessions list and invoke Rename{0}, double-click its title, or open its context menu and choose Rename. Type the new title, then press Enter to confirm or Escape to cancel. From the main chat transcript or input, invoking Rename edits the header title inline when it is visible and opens a prompt otherwise.", ``)); content.push(localize('sessionsChat.renameChat', "When Rename is available for a non-main chat, focus its nested row in the Sessions list and invoke Rename{0} or double-click its title to rename it inline. Type the new title, then press Enter to confirm or Escape to cancel. From the chat transcript or input, invoking Rename opens a prompt instead.", ``)); content.push(localize('sessionsChat.archiveSession', "To archive or mark one or more sessions as done, focus them in the Sessions list and invoke Archive or Mark as Done{0}.", ``)); content.push(localize('sessionsChat.deleteSession', "To permanently delete a session, open its context menu and choose Delete. This is destructive and cannot be undone.")); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts b/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts index ffade60fdc98d..322cbf0ac457c 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts @@ -64,7 +64,7 @@ import { logSessionsInteraction, SessionsInteractionSource } from '../../../comm import { NEW_SESSION_ACTION_ID } from '../../chat/common/constants.js'; import { groupSessionsForPicker } from './sessionsPicker.js'; import { getSessionConversationActionId, isSessionConversationSideChat, SESSION_CONVERSATION_SIDE_CHATS_GROUP } from '../../../browser/sessionConversationGroups.js'; -import { ISessionChatItem, RENAME_SESSION_LIST_CHAT_ACTION_ID, SessionChatItemCanDeleteContext, SessionChatItemCanRenameContext, SessionChatItemIsUntitledContext, SessionsList, SessionsListFocusedChatItemContext } from './views/sessionsList.js'; +import { ISessionChatItem, SessionChatItemCanDeleteContext, SessionChatItemCanRenameContext, SessionChatItemIsUntitledContext, SessionsList, SessionsListFocusedChatItemContext } from './views/sessionsList.js'; import { SessionsView, SessionsViewId } from './views/sessionsView.js'; import './media/newSessionActionViewItem.css'; import { INewSessionComposerService } from '../../chat/browser/newSessionComposerService.js'; @@ -637,6 +637,12 @@ registerAction2(class RenameChatAction extends Action2 { ), ), }, + menu: { + id: Menus.SessionChatItemContext, + group: '1_chat', + order: 1, + when: ContextKeyExpr.and(SessionChatItemCanRenameContext, SessionChatItemIsUntitledContext.negate()), + }, }); } @@ -655,29 +661,6 @@ registerAction2(class RenameChatAction extends Action2 { } }); -registerAction2(class RenameSessionListChatAction extends Action2 { - constructor() { - super({ - id: RENAME_SESSION_LIST_CHAT_ACTION_ID, - title: localize2('renameChat', "Rename..."), - f1: false, - menu: { - id: Menus.SessionChatItemContext, - group: '1_chat', - order: 1, - when: ContextKeyExpr.and(SessionChatItemCanRenameContext, SessionChatItemIsUntitledContext.negate()), - }, - }); - } - - override async run(accessor: ServicesAccessor, context?: ISessionChatItem): Promise { - if (!context) { - return; - } - await renameChatWithQuickInput(accessor, context); - } -}); - registerAction2(class OpenSessionListChatToSideAction extends Action2 { constructor() { super({ @@ -1785,6 +1768,16 @@ registerAction2(class RenameSessionHeaderAction extends Action2 { id: 'sessions.sessionHeader.rename', title: localize2('renameSessionHeader', "Rename..."), icon: Codicon.edit, + keybinding: { + primary: KeyCode.F2, + weight: KeybindingWeight.SessionsContrib + 1, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + SessionsFocusContext, + SessionSupportsRenameContext, + SessionFocusedChatIsRenameTargetContext.negate(), + ), + }, menu: [{ id: Menus.SessionHeaderContext, group: '2_edit', @@ -1800,6 +1793,7 @@ registerAction2(class RenameSessionHeaderAction extends Action2 { } override async run(accessor: ServicesAccessor, session: IActiveSession | undefined): Promise { + session ??= accessor.get(ISessionsService).activeSession.get(); if (!session) { return; } diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index 735facd053e40..1b724d3e45769 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -34,7 +34,7 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { MarshalledId } from '../../../../../base/common/marshallingIds.js'; import { SessionProviderIdContext, SessionSupportsDeleteContext, SessionSupportsMultipleChatsContext, SessionSupportsRenameContext, SessionTypeContext, IsPhoneLayoutContext, IsQuickChatSessionContext, SessionIsArchivedContext, SessionIsReadContext, SessionHasPullRequestContext } from '../../../../common/contextkeys.js'; -import { ARCHIVE_SESSION_COMMAND_ID, RENAME_SESSION_COMMAND_ID } from '../../../../common/sessionCommands.js'; +import { ARCHIVE_SESSION_COMMAND_ID, RENAME_CHAT_COMMAND_ID, RENAME_SESSION_COMMAND_ID } from '../../../../common/sessionCommands.js'; import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; @@ -127,7 +127,6 @@ const EMPTY_GUIDE_SESSION_IDS: ReadonlySet = new Set(); export const SessionItemContextMenuId = MenuId.SessionItemContextMenu; export const SessionSectionToolbarMenuId = new MenuId('SessionSectionToolbar'); export const SessionGroupToolbarMenuId = new MenuId('SessionGroupToolbar'); -export const RENAME_SESSION_LIST_CHAT_ACTION_ID = 'sessions.list.renameChat'; export const NEW_SESSION_FOR_WORKSPACE_ACTION_ID = 'sessionsView.sectionNewSession'; /** Controls whether the empty default Chats group is shown in the sessions list. */ @@ -4792,7 +4791,7 @@ export class SessionsList extends Disposable implements ISessionsList { [SessionProviderIdContext.key, element.session.providerId], ]); const menu = this.menuService.createMenu(Menus.SessionChatItemContext, contextKeyService); - const wrapAction = (action: IAction): IAction => action.id === RENAME_SESSION_LIST_CHAT_ACTION_ID + const wrapAction = (action: IAction): IAction => action.id === RENAME_CHAT_COMMAND_ID ? toAction({ id: action.id, label: action.label, diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsListContextMenu.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsListContextMenu.test.ts index bbade585dbb16..4f66917dd816e 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsListContextMenu.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsListContextMenu.test.ts @@ -17,14 +17,14 @@ import { CommandsRegistry, ICommandService } from '../../../../../platform/comma import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; -import { RENAME_SESSION_COMMAND_ID } from '../../../../common/sessionCommands.js'; +import { RENAME_CHAT_COMMAND_ID, RENAME_SESSION_COMMAND_ID } from '../../../../common/sessionCommands.js'; import { ISessionGroup, ISessionGroupsService } from '../../../../services/sessions/browser/sessionGroupsService.js'; import { ISessionsPartService } from '../../../../services/sessions/browser/sessionsPartService.js'; import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; import { ChatInteractivity, IChat, ISession, SessionStatus } from '../../../../services/sessions/common/session.js'; import type { SessionView } from '../../../../browser/parts/sessionView.js'; import { Menus } from '../../../../browser/menus.js'; -import { RENAME_SESSION_LIST_CHAT_ACTION_ID, SessionsGrouping, SessionsList, SessionsSorting } from '../../browser/views/sessionsList.js'; +import { SessionsGrouping, SessionsList, SessionsSorting } from '../../browser/views/sessionsList.js'; import { createListHarness, createSession, createTestSession } from './sessionsListTestUtils.js'; import '../../browser/sessionsActions.js'; @@ -203,14 +203,15 @@ suite('Sessions list context menus', () => { mainChat: constObservable(mainChat), }; const chatRenameAction = { - id: RENAME_SESSION_LIST_CHAT_ACTION_ID, + id: RENAME_CHAT_COMMAND_ID, run: () => fallbackRuns++, }; const chatList = createList(false, false, SessionsGrouping.Date, [chatSession], [chatRenameAction]); const chatRow = chatList.container.querySelector('.session-chat-item'); assert.ok(chatRow); dispatchContextMenu(chatRow); - const chatRename = chatList.contextMenuService.delegate!.getActions().find(action => action.id === RENAME_SESSION_LIST_CHAT_ACTION_ID); + const chatRename = chatList.contextMenuService.delegate!.getActions().find(action => action.id === RENAME_CHAT_COMMAND_ID); + assert.ok(chatRename); chatList.managementService.sessions = [{ ...chatSession, chats: constObservable([mainChat, peerChat]), @@ -330,7 +331,7 @@ suite('Sessions list context menus', () => { } }); }); - const coreActionIds = new Set(['sessions.list.renameChat', 'sessions.list.openChatToSide', 'sessions.list.deleteChat']); + const coreActionIds = new Set([RENAME_CHAT_COMMAND_ID, 'sessions.list.openChatToSide', 'sessions.list.deleteChat']); const menuItems = MenuRegistry.getMenuItems(Menus.SessionChatItemContext) .filter(isIMenuItem) .filter(item => coreActionIds.has(item.command.id)); @@ -341,16 +342,16 @@ suite('Sessions list context menus', () => { order: item.order, when: item.when?.serialize(), })), [ - { id: 'sessions.list.renameChat', title: 'Rename...', group: '1_chat', order: 1, when: 'sessionChatItem.canRename && !sessionChatItem.isUntitled' }, + { id: RENAME_CHAT_COMMAND_ID, title: 'Rename...', group: '1_chat', order: 1, when: 'sessionChatItem.canRename && !sessionChatItem.isUntitled' }, { id: 'sessions.list.openChatToSide', title: 'Open to the Side', group: '1_chat', order: 2, when: undefined }, { id: 'sessions.list.deleteChat', title: 'Delete...', group: '2_delete', order: 1, when: 'sessionChatItem.canDelete' }, ]); const chatContext = { session, chat: peer }; - for (const actionId of ['sessions.list.renameChat', 'sessions.list.openChatToSide', 'sessions.list.deleteChat']) { + for (const actionId of [RENAME_CHAT_COMMAND_ID, 'sessions.list.openChatToSide', 'sessions.list.deleteChat']) { await harness.instantiationService.invokeFunction(CommandsRegistry.getCommand(actionId)!.handler, chatContext); } const readOnlyContext = { session, chat: nonDeletable }; - await harness.instantiationService.invokeFunction(CommandsRegistry.getCommand('sessions.list.renameChat')!.handler, readOnlyContext); + await harness.instantiationService.invokeFunction(CommandsRegistry.getCommand(RENAME_CHAT_COMMAND_ID)!.handler, readOnlyContext); await harness.instantiationService.invokeFunction(CommandsRegistry.getCommand('sessions.list.deleteChat')!.handler, readOnlyContext); assert.deepStrictEqual({ diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts index 0752fab6c4ce5..5a73122b99002 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts @@ -768,8 +768,12 @@ suite('Sessions rename', () => { const instantiationService = disposables.add(new TestInstantiationService()); const commandService = new TestCommandService(); const sessionData = createTestSession('Existing'); + const session = upcastPartial({ ...sessionData.session }); let inlineRenameCalls = 0; instantiationService.stub(ICommandService, commandService); + instantiationService.stub(ISessionsService, new class extends mock() { + override readonly activeSession = constObservable(session); + }); instantiationService.stub(ISessionsPartService, new class extends mock() { override getSessionView() { if (inlineRename === undefined) { @@ -785,7 +789,7 @@ suite('Sessions rename', () => { }); const handler = CommandsRegistry.getCommand('sessions.sessionHeader.rename')?.handler; assert.ok(handler); - return { handler, instantiationService, commandService, session: sessionData.session, inlineRenameCalls: () => inlineRenameCalls }; + return { handler, instantiationService, commandService, session, inlineRenameCalls: () => inlineRenameCalls }; } test('renames inline in the header and only prompts when that is not possible', async () => { @@ -812,7 +816,7 @@ suite('Sessions rename', () => { inline: { calls: 1, prompts: [] }, headerUnavailable: { calls: 1, prompts: [{ commandId: RENAME_SESSION_COMMAND_ID, args: [headerUnavailable.session] }] }, noView: { calls: 0, prompts: [{ commandId: RENAME_SESSION_COMMAND_ID, args: [noView.session] }] }, - withoutSession: { calls: 0, prompts: [] }, + withoutSession: { calls: 1, prompts: [] }, }); }); }); @@ -866,6 +870,7 @@ suite('Sessions rename', () => { hasInlineChatRenameInstructions: content.includes('focus its nested row') && content.includes('double-click its title to rename it inline'), hasSessionRenameKeybinding: content.includes(``), hasInlineRenameInstructions: content.includes('press Enter to confirm or Escape to cancel'), + hasHeaderRenameInstructions: content.includes('edits the header title inline when it is visible and opens a prompt otherwise'), hasChatRenameKeybinding: content.includes(``), hasArchiveKeybinding: content.includes(``), hasPermanentDelete: content.includes('open its context menu and choose Delete'), @@ -888,6 +893,7 @@ suite('Sessions rename', () => { hasInlineChatRenameInstructions: true, hasSessionRenameKeybinding: true, hasInlineRenameInstructions: true, + hasHeaderRenameInstructions: true, hasChatRenameKeybinding: true, hasArchiveKeybinding: true, hasPermanentDelete: true, diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts index 2ace263edb826..d8d60233373bf 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts @@ -55,12 +55,14 @@ suite('Sessions - Session management actions', () => { test('scopes Rename and Archive keybindings to their Agents Window surfaces', () => { const renameRule = getKeybindingRule(RENAME_SESSION_COMMAND_ID, KeyCode.F2); const renameChatRule = getKeybindingRule(RENAME_CHAT_COMMAND_ID, KeyCode.F2); + const renameHeaderRule = getKeybindingRule('sessions.sessionHeader.rename', KeyCode.F2); const archiveSessionRule = getKeybindingRule(ARCHIVE_SESSION_COMMAND_ID, KeyCode.Delete); const archiveSessionMacRule = getKeybindingRule(ARCHIVE_SESSION_COMMAND_ID, KeyMod.CtrlCmd | KeyCode.Backspace, OperatingSystem.Macintosh); const deleteSessionRule = getKeybindingRule('sessionsViewPane.deleteSession', KeyCode.Delete); const deleteChatRule = getKeybindingRule(DELETE_CHAT_COMMAND_ID, KeyCode.Delete); assert.ok(renameRule?.when); assert.ok(renameChatRule?.when); + assert.ok(renameHeaderRule?.when); assert.ok(archiveSessionRule?.when); assert.ok(archiveSessionMacRule?.when); assert.ok(deleteChatRule?.when); @@ -86,8 +88,10 @@ suite('Sessions - Session management actions', () => { assert.deepStrictEqual({ renameWeight: renameRule.weight1, renameChatWeight: renameChatRule.weight1, + renameHeaderWeight: renameHeaderRule.weight1, renameSessionRow: evaluate(renameRule, sessionsList), renameChatOnSessionRow: evaluate(renameChatRule, sessionsList), + renameHeaderOnSessionRow: evaluate(renameHeaderRule, sessionsList), renameNestedChatAsSession: evaluate(renameRule, nestedChat), renameNestedChat: evaluate(renameChatRule, nestedChat), renameInListFindInput: evaluate(renameRule, { ...sessionsList, [InputFocusedContext.key]: true }), @@ -96,6 +100,8 @@ suite('Sessions - Session management actions', () => { renameMainTranscriptAsChat: evaluate(renameChatRule, chatTranscript), renamePeerTranscriptAsSession: evaluate(renameRule, peerChatTranscript), renamePeerTranscriptAsChat: evaluate(renameChatRule, peerChatTranscript), + renameHeaderInMainTranscript: evaluate(renameHeaderRule, chatTranscript), + renameHeaderInPeerTranscript: evaluate(renameHeaderRule, peerChatTranscript), renamePeerChatInput: evaluate(renameChatRule, { ...peerChatTranscript, [ChatContextKeys.inChatInput.key]: true, [InputFocusedContext.key]: true }), renameUnsupportedPeerAsChat: evaluate(renameChatRule, { ...peerChatTranscript, [SessionSupportsRenameContext.key]: false }), renameOutsideAgentsWindow: evaluate(renameRule, { [ChatContextKeys.inChatSession.key]: true, [SessionSupportsRenameContext.key]: true }), @@ -112,8 +118,10 @@ suite('Sessions - Session management actions', () => { }, { renameWeight: KeybindingWeight.SessionsContrib, renameChatWeight: KeybindingWeight.SessionsContrib + 10, + renameHeaderWeight: KeybindingWeight.SessionsContrib + 1, renameSessionRow: true, renameChatOnSessionRow: false, + renameHeaderOnSessionRow: false, renameNestedChatAsSession: true, renameNestedChat: true, renameInListFindInput: false, @@ -122,6 +130,8 @@ suite('Sessions - Session management actions', () => { renameMainTranscriptAsChat: false, renamePeerTranscriptAsSession: true, renamePeerTranscriptAsChat: true, + renameHeaderInMainTranscript: true, + renameHeaderInPeerTranscript: false, renamePeerChatInput: true, renameUnsupportedPeerAsChat: true, renameOutsideAgentsWindow: false, From 23b314554eec8c66526fa260082e84cdcea48374 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 20 Sep 2026 19:40:39 +0200 Subject: [PATCH 2/2] sessions: support F2 on chat tabs Use the shared chat rename command from tab menus and let focused-tab F2 preserve inline editing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/parts/chatCompositeBar.ts | 38 +++++++++--- .../sessions/browser/parts/chatGroupView.ts | 8 +++ .../sessions/browser/parts/chatGroupsView.ts | 9 +++ src/vs/sessions/browser/parts/sessionView.ts | 8 +++ .../browser/sessionsChatAccessibilityHelp.ts | 2 +- .../sessions/browser/sessionsActions.ts | 10 ++- .../test/browser/sessionsRename.test.ts | 2 +- .../sessionsSessionManagementActions.test.ts | 47 ++++++++++++-- .../test/browser/chatCompositeBar.test.ts | 61 ++++++++++++++++++- 9 files changed, 163 insertions(+), 22 deletions(-) diff --git a/src/vs/sessions/browser/parts/chatCompositeBar.ts b/src/vs/sessions/browser/parts/chatCompositeBar.ts index 4a9a41432df2f..5d5b586927b77 100644 --- a/src/vs/sessions/browser/parts/chatCompositeBar.ts +++ b/src/vs/sessions/browser/parts/chatCompositeBar.ts @@ -37,9 +37,10 @@ import { applySessionBarThemeColors } from './sessionBarStyles.js'; import { ISessionsProvidersService } from '../../services/sessions/browser/sessionsProvidersService.js'; import { isAgentHostProvider } from '../../common/agentHostSessionsProvider.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; -import { CLOSE_CHAT_COMMAND_ID, COPY_AGENT_HOST_CHAT_LINK_COMMAND_ID } from '../../common/sessionCommands.js'; +import { CLOSE_CHAT_COMMAND_ID, COPY_AGENT_HOST_CHAT_LINK_COMMAND_ID, RENAME_CHAT_COMMAND_ID } from '../../common/sessionCommands.js'; import { getSessionConversationStatusAriaLabel } from '../sessionConversationGroups.js'; import { IEditorGroupsService } from '../../../workbench/services/editor/common/editorGroupsService.js'; +import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; interface IChatTab { readonly chat: IChat; @@ -145,6 +146,7 @@ export class ChatCompositeBar extends Disposable { @ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService, @ICommandService private readonly _commandService: ICommandService, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); @@ -504,8 +506,10 @@ export class ChatCompositeBar extends Disposable { this._delegate?.onTabDragEnd?.(); })); - const renameAction = this._tabDisposables.add(new Action('sessionCompositeBar.renameChat', localize('renameChat', "Rename"), undefined, true, async () => { - this._startTabEditing(chatTab); + const renameAction = this._tabDisposables.add(new Action(RENAME_CHAT_COMMAND_ID, localize('renameChat', "Rename..."), undefined, true, async () => { + if (session) { + await this._commandService.executeCommand(RENAME_CHAT_COMMAND_ID, { session, chat, inline: true }); + } })); const copyLinkAction = this._tabDisposables.add(new Action(COPY_AGENT_HOST_CHAT_LINK_COMMAND_ID, localize('copyChatLink', "Copy Link"), undefined, true, async () => { @@ -551,7 +555,8 @@ export class ChatCompositeBar extends Disposable { provider && isAgentHostProvider(provider) ? [copyLinkAction] : [], capabilities.canDelete ? [deleteAction] : [], ); - } + }, + getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id) ?? undefined, }); })); @@ -578,17 +583,29 @@ export class ChatCompositeBar extends Disposable { return provider && isAgentHostProvider(provider) ? provider.getBackendChatResource(chat.resource) : undefined; } - /** - * Start an inline rename for the given tab. Enter commits via - * {@link ISessionsManagementService.renameChat}; Escape or blur cancels. - */ - private _startTabEditing(chatTab: IChatTab): void { + startFocusedTabEditing(): boolean { + const chatTab = this._tabs.find(tab => tab.element === tab.element.ownerDocument.activeElement); + if (!chatTab) { + return false; + } + return this._startTabEditing(chatTab); + } + + startTabEditing(chatResource: URI): boolean { + const chatTab = this._tabs.find(tab => tab.chat.resource.toString() === chatResource.toString()); + return chatTab ? this._startTabEditing(chatTab) : false; + } + + private _startTabEditing(chatTab: IChatTab): boolean { const delegate = this._delegate; if (!delegate || this._editingTab) { - return; + return false; } const { chat, element: tab, inputContainer } = chatTab; + if (chat.resource.toString() === delegate.mainChatResource.get() || chat.status.get() === SessionStatus.Untitled || !getChatCapabilities(chat, delegate.session, undefined).canRename) { + return false; + } const initialTitle = chat.title.get(); this._editingTab = chatTab; @@ -640,6 +657,7 @@ export class ChatCompositeBar extends Disposable { store.add(addDisposableListener(inputBox.element, EventType.CLICK, e => e.stopPropagation())); store.add(addDisposableListener(inputBox.element, EventType.DBLCLICK, e => e.stopPropagation())); + return true; } private _cancelTabEditing(): void { diff --git a/src/vs/sessions/browser/parts/chatGroupView.ts b/src/vs/sessions/browser/parts/chatGroupView.ts index 389ca9db6edeb..58e71ca24c12d 100644 --- a/src/vs/sessions/browser/parts/chatGroupView.ts +++ b/src/vs/sessions/browser/parts/chatGroupView.ts @@ -182,6 +182,14 @@ export class ChatGroupView extends Disposable implements ISerializableView { this._compositeBar.setAriaLabel(localize('chatGroupTabsAriaLabel', "Chats, Group {0} of {1}", index + 1, count)); } + startFocusedChatTitleEditing(): boolean { + return this._compositeBar.startFocusedTabEditing(); + } + + startChatTitleEditing(chatResource: URI): boolean { + return this._compositeBar.startTabEditing(chatResource); + } + /** Sets (or clears) the group this view renders. */ setContext(context: IChatGroupContext | undefined): void { this._contextDisposables.clear(); diff --git a/src/vs/sessions/browser/parts/chatGroupsView.ts b/src/vs/sessions/browser/parts/chatGroupsView.ts index 0ab316e835dcf..95129f88fc388 100644 --- a/src/vs/sessions/browser/parts/chatGroupsView.ts +++ b/src/vs/sessions/browser/parts/chatGroupsView.ts @@ -702,6 +702,15 @@ export class ChatGroupsView extends Themable { return group.chats.get().find(chat => chat.resource.toString() === activeResource); } + startFocusedChatTitleEditing(): boolean { + return this._getFocusedGroup()?.view.startFocusedChatTitleEditing() ?? false; + } + + startChatTitleEditing(chatResource: URI): boolean { + const group = this._groups.find(group => group.chats.get().some(chat => chat.resource.toString() === chatResource.toString())); + return group?.view.startChatTitleEditing(chatResource) ?? false; + } + private _getFocusedGroup(): IGroupEntry | undefined { return this._groups.find(group => isAncestorOfActiveElement(group.view.element)); } diff --git a/src/vs/sessions/browser/parts/sessionView.ts b/src/vs/sessions/browser/parts/sessionView.ts index 6f93caa58109e..299a6e1d902a0 100644 --- a/src/vs/sessions/browser/parts/sessionView.ts +++ b/src/vs/sessions/browser/parts/sessionView.ts @@ -283,6 +283,14 @@ export class SessionView extends Disposable implements ISerializableView { return this._isVisible && this._header.startTitleEditing(); } + startFocusedChatTitleEditing(): boolean { + return this._isVisible && this._groupsView.startFocusedChatTitleEditing(); + } + + startChatTitleEditing(chatResource: URI): boolean { + return this._isVisible && this._groupsView.startChatTitleEditing(chatResource); + } + getFocusedChat(): IChat | undefined { return this._groupsView.getFocusedChat(); } diff --git a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts index 299a266740acd..8c1734d217904 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts @@ -114,7 +114,7 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat content.push(localize('sessionsChat.navigatePreviousSession', "Navigate to the previous session in the list{0}.", '')); content.push(localize('sessionsChat.navigateNextSession', "Navigate to the next session in the list{0}.", '')); content.push(localize('sessionsChat.renameSession', "To rename a session inline, focus its row in the Sessions list and invoke Rename{0}, double-click its title, or open its context menu and choose Rename. Type the new title, then press Enter to confirm or Escape to cancel. From the main chat transcript or input, invoking Rename edits the header title inline when it is visible and opens a prompt otherwise.", ``)); - content.push(localize('sessionsChat.renameChat', "When Rename is available for a non-main chat, focus its nested row in the Sessions list and invoke Rename{0} or double-click its title to rename it inline. Type the new title, then press Enter to confirm or Escape to cancel. From the chat transcript or input, invoking Rename opens a prompt instead.", ``)); + content.push(localize('sessionsChat.renameChat', "When Rename is available for a non-main chat, focus its tab or nested row in the Sessions list and invoke Rename{0}, or double-click its title, to rename it inline. Type the new title, then press Enter to confirm or Escape to cancel. From the chat transcript or input, invoking Rename opens a prompt instead.", ``)); content.push(localize('sessionsChat.archiveSession', "To archive or mark one or more sessions as done, focus them in the Sessions list and invoke Archive or Mark as Done{0}.", ``)); content.push(localize('sessionsChat.deleteSession', "To permanently delete a session, open its context menu and choose Delete. This is destructive and cannot be undone.")); content.push(localize('sessionsChat.changes', "Focus the Changes view{0}.", '')); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts b/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts index 322cbf0ac457c..a4e43752451c9 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsActions.ts @@ -33,7 +33,6 @@ import { EditorAreaFocusContext, FocusedViewContext, IsAuxiliaryWindowContext, I import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; import { getQuickNavigateHandler, inQuickPickContext } from '../../../../workbench/browser/quickaccess.js'; -import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { Menus } from '../../../browser/menus.js'; import { SessionsCategories } from '../../../common/categories.js'; import { CanGoBackContext, CanGoForwardContext, SessionProviderIdContext, MultipleSessionsVisibleContext, SessionIsArchivedContext, SessionIsCreatedContext, SessionIsMaximizedContext, SessionIsStickyContext, SessionsFocusContext, SessionSupportsMultipleChatsContext, SessionSupportsRenameContext, SessionsWelcomeVisibleContext, SessionIdContext, SessionHasMultipleCommittedChatsContext, SessionHasMultipleOpenChatsContext, SessionsPickerVisibleContext, SessionActiveChatIsClosableContext, SessionFocusedChatIsRenameTargetContext, SessionActiveChatIsDeletableContext, SessionChatsPickerVisibleContext, SessionHasSideChatsContext, SessionsTitleBarNewSessionEnabledContext, SessionsEditorScopeContext, SessionsHasClosedItemContext, IsQuickChatSessionContext, SessionsListPromoteNewChatActionContext } from '../../../common/contextkeys.js'; @@ -563,6 +562,7 @@ const CHAT_TAB_KEYBINDING_WEIGHT = KeybindingWeight.SessionsContrib + 10; interface IChatRenameContext { readonly session: ISession; readonly chat: IChat; + readonly inline?: boolean; } function getSessionsList(accessor: ServicesAccessor): SessionsList | undefined { @@ -632,7 +632,7 @@ registerAction2(class RenameChatAction extends Action2 { when: ContextKeyExpr.and( IsSessionsWindowContext, ContextKeyExpr.or( - ContextKeyExpr.and(ChatContextKeys.inChatSession, SessionFocusedChatIsRenameTargetContext), + SessionFocusedChatIsRenameTargetContext, ContextKeyExpr.and(FocusedViewContext.isEqualTo(SessionsViewId), WorkbenchListFocusContextKey, SessionsListFocusedChatItemContext), ), ), @@ -654,6 +654,12 @@ registerAction2(class RenameChatAction extends Action2 { return; } } + if (context?.inline && accessor.get(ISessionsPartService).getSessionView(context.session.sessionId)?.startChatTitleEditing(context.chat.resource)) { + return; + } + if (!context && accessor.get(ISessionsPartService).getFocusedSessionView()?.startFocusedChatTitleEditing?.()) { + return; + } const target = getChatRenameContext(accessor, context); if (target) { await renameChatWithQuickInput(accessor, target); diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts index 5a73122b99002..c666f1b9454fd 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts @@ -867,7 +867,7 @@ suite('Sessions rename', () => { hasMainChatFocus: content.includes('main chat transcript or input'), hasPeerChatFocus: content.includes('non-main chat') && content.includes('nested row'), scopesChatRenameToAvailability: content.includes('When Rename is available for a non-main chat'), - hasInlineChatRenameInstructions: content.includes('focus its nested row') && content.includes('double-click its title to rename it inline'), + hasInlineChatRenameInstructions: content.includes('focus its tab or nested row') && content.includes('double-click its title'), hasSessionRenameKeybinding: content.includes(``), hasInlineRenameInstructions: content.includes('press Enter to confirm or Escape to cancel'), hasHeaderRenameInstructions: content.includes('edits the header title inline when it is visible and opens a prompt otherwise'), diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts index d8d60233373bf..ce6db9961ce92 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsSessionManagementActions.test.ts @@ -83,6 +83,11 @@ suite('Sessions - Session management actions', () => { [SessionFocusedChatIsRenameTargetContext.key]: false, }; const peerChatTranscript = { ...chatTranscript, [SessionFocusedChatIsRenameTargetContext.key]: true }; + const peerChatTab = { + ...sessionsWindow, + [SessionsFocusContext.key]: true, + [SessionFocusedChatIsRenameTargetContext.key]: true, + }; const nestedChat = { ...sessionsList, [SessionsListFocusedChatItemContext.key]: true }; assert.deepStrictEqual({ @@ -100,6 +105,7 @@ suite('Sessions - Session management actions', () => { renameMainTranscriptAsChat: evaluate(renameChatRule, chatTranscript), renamePeerTranscriptAsSession: evaluate(renameRule, peerChatTranscript), renamePeerTranscriptAsChat: evaluate(renameChatRule, peerChatTranscript), + renamePeerTabAsChat: evaluate(renameChatRule, peerChatTab), renameHeaderInMainTranscript: evaluate(renameHeaderRule, chatTranscript), renameHeaderInPeerTranscript: evaluate(renameHeaderRule, peerChatTranscript), renamePeerChatInput: evaluate(renameChatRule, { ...peerChatTranscript, [ChatContextKeys.inChatInput.key]: true, [InputFocusedContext.key]: true }), @@ -130,6 +136,7 @@ suite('Sessions - Session management actions', () => { renameMainTranscriptAsChat: false, renamePeerTranscriptAsSession: true, renamePeerTranscriptAsChat: true, + renamePeerTabAsChat: true, renameHeaderInMainTranscript: true, renameHeaderInPeerTranscript: false, renamePeerChatInput: true, @@ -148,11 +155,13 @@ suite('Sessions - Session management actions', () => { }); }); - function createActionHarness(focusedSessions: readonly ISession[] | undefined, activeSession: IActiveSession | undefined, focusedChat?: ISessionChatItem, focusedGroupChat?: IChat) { + function createActionHarness(focusedSessions: readonly ISession[] | undefined, activeSession: IActiveSession | undefined, focusedChat?: ISessionChatItem, focusedGroupChat?: IChat, renameFocusedTab = false) { const instantiationService = disposables.add(new TestInstantiationService()); const managementService = new TestSessionsManagementService([]); const inlineRenamedSessions: ISession[] = []; const inlineRenamedChats: ISessionChatItem[] = []; + const inlineRenamedTabs: URI[] = []; + let inlineRenamedFocusedTabs = 0; const sessionsControl = upcastPartial({ getFocusedSessions: () => focusedSessions, getFocusedChatItem: () => focusedChat, @@ -172,11 +181,27 @@ suite('Sessions - Session management actions', () => { instantiationService.stub(ISessionsService, upcastPartial({ activeSession: constObservable(activeSession ? upcastPartial(activeSession) : undefined), })); + const sessionView = activeSession ? upcastPartial({ + getSession: () => activeSession, + getFocusedChat: () => focusedGroupChat, + startChatTitleEditing: chatResource => { + inlineRenamedTabs.push(chatResource); + return true; + }, + startFocusedChatTitleEditing: () => { + if (!renameFocusedTab) { + return false; + } + inlineRenamedFocusedTabs++; + return true; + }, + }) : undefined; instantiationService.stub(ISessionsPartService, new class extends mock() { + override getSessionView(sessionId: string | undefined): SessionView | undefined { + return sessionView?.getSession()?.sessionId === sessionId ? sessionView : undefined; + } override getFocusedSessionView(): SessionView | undefined { - return focusedGroupChat && activeSession - ? upcastPartial({ getSession: () => activeSession, getFocusedChat: () => focusedGroupChat }) - : undefined; + return focusedGroupChat ? sessionView : undefined; } }()); instantiationService.stub(ISessionsManagementService, managementService); @@ -185,7 +210,7 @@ suite('Sessions - Session management actions', () => { input: async () => 'Renamed', })); - return { instantiationService, managementService, inlineRenamedSessions, inlineRenamedChats }; + return { instantiationService, managementService, inlineRenamedSessions, inlineRenamedChats, inlineRenamedTabs, inlineRenamedFocusedTabs: () => inlineRenamedFocusedTabs }; } test('routes session and chat rename commands to their focused targets', async () => { @@ -208,6 +233,8 @@ suite('Sessions - Session management actions', () => { activeChat: constObservable(peerChat), }); const chatHarness = createActionHarness(undefined, activeSession, undefined, peerChat); + const chatTabHarness = createActionHarness(undefined, activeSession, undefined, peerChat, true); + const chatTabMenuHarness = createActionHarness(undefined, activeSession); const nestedChatHarness = createActionHarness([], listActiveSession, { session: activeSession, chat: peerChat }); const archiveSession = createTestSession('Archive target').session; const archivedSession = createTestSession('Already archived', { isArchived: true }).session; @@ -221,6 +248,8 @@ suite('Sessions - Session management actions', () => { await renameSessionHandler(listHarness.instantiationService); await renameSessionHandler(chatHarness.instantiationService); await renameChatHandler(chatHarness.instantiationService); + await renameChatHandler(chatTabHarness.instantiationService); + await renameChatHandler(chatTabMenuHarness.instantiationService, { session: activeSession, chat: peerChat, inline: true }); await renameChatHandler(nestedChatHarness.instantiationService); await archiveHarness.instantiationService.invokeFunction(accessor => new ArchiveSessionAction().run(accessor)); await inactiveArchiveHarness.instantiationService.invokeFunction(accessor => new ArchiveSessionAction().run(accessor)); @@ -230,6 +259,10 @@ suite('Sessions - Session management actions', () => { listPromptRename: listHarness.managementService.renamed, sessionRenameFromChat: chatHarness.managementService.renamed.map(({ session, title }) => ({ sessionId: session.sessionId, title })), activeChatRename: chatHarness.managementService.renamedChats.map(({ session, chatResource, title }) => ({ sessionId: session.sessionId, chatResource: chatResource.toString(), title })), + activeChatTabInlineRename: chatTabHarness.inlineRenamedFocusedTabs(), + activeChatTabPromptRename: chatTabHarness.managementService.renamedChats, + chatTabMenuInlineRename: chatTabMenuHarness.inlineRenamedTabs.map(resource => resource.toString()), + chatTabMenuPromptRename: chatTabMenuHarness.managementService.renamedChats, nestedChatInlineRename: nestedChatHarness.inlineRenamedChats.map(item => item.chat.resource.toString()), nestedChatPromptRename: nestedChatHarness.managementService.renamedChats, archived: archiveHarness.managementService.archived.map(session => session.sessionId), @@ -239,6 +272,10 @@ suite('Sessions - Session management actions', () => { listPromptRename: [], sessionRenameFromChat: [{ sessionId: activeSession.sessionId, title: 'Renamed' }], activeChatRename: [{ sessionId: activeSession.sessionId, chatResource: peerChat.resource.toString(), title: 'Renamed' }], + activeChatTabInlineRename: 1, + activeChatTabPromptRename: [], + chatTabMenuInlineRename: [peerChat.resource.toString()], + chatTabMenuPromptRename: [], nestedChatInlineRename: [peerChat.resource.toString()], nestedChatPromptRename: [], archived: [archiveSession.sessionId], diff --git a/src/vs/sessions/test/browser/chatCompositeBar.test.ts b/src/vs/sessions/test/browser/chatCompositeBar.test.ts index bb59df1d74ac8..2ffe8cc57f74e 100644 --- a/src/vs/sessions/test/browser/chatCompositeBar.test.ts +++ b/src/vs/sessions/test/browser/chatCompositeBar.test.ts @@ -4,17 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { IContextMenuDelegate } from '../../../base/browser/contextmenu.js'; import { addDisposableListener, EventType } from '../../../base/browser/dom.js'; import { mainWindow } from '../../../base/browser/window.js'; import { Emitter, Event } from '../../../base/common/event.js'; +import { ResolvedKeybinding } from '../../../base/common/keybindings.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; import { constObservable, IObservable, ISettableObservable, observableValue } from '../../../base/common/observable.js'; import { isLinux } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; -import { mock } from '../../../base/test/common/mock.js'; +import { mock, upcastPartial } from '../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; +import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js'; import { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; import { IMenu, IMenuService, MenuItemAction } from '../../../platform/actions/common/actions.js'; import { DEFAULT_EDITOR_PART_OPTIONS } from '../../../workbench/browser/parts/editor/editor.js'; import { IEditorPartOptions, IEditorPartOptionsChangeEvent } from '../../../workbench/common/editor.js'; @@ -22,7 +26,7 @@ import { IEditorGroupsService } from '../../../workbench/services/editor/common/ import { workbenchInstantiationService } from '../../../workbench/test/browser/workbenchTestServices.js'; import { ChatCompositeBar, IChatCompositeBarDelegate } from '../../browser/parts/chatCompositeBar.js'; import { getSessionChatDragData, isSessionChatDrag } from '../../browser/dnd.js'; -import { CLOSE_CHAT_COMMAND_ID } from '../../common/sessionCommands.js'; +import { CLOSE_CHAT_COMMAND_ID, RENAME_CHAT_COMMAND_ID } from '../../common/sessionCommands.js'; import { ISessionsProvidersService } from '../../services/sessions/browser/sessionsProvidersService.js'; import { ISessionsPartService } from '../../services/sessions/browser/sessionsPartService.js'; import { ISessionsService } from '../../services/sessions/browser/sessionsService.js'; @@ -69,6 +73,16 @@ class TestCommandService extends mock() { } } +class TestContextMenuService extends mock() { + override readonly onDidShowContextMenu = Event.None; + override readonly onDidHideContextMenu = Event.None; + delegate: IContextMenuDelegate | undefined; + + override showContextMenu(delegate: IContextMenuDelegate): void { + this.delegate = delegate; + } +} + class TestSessionsService extends mock() { readonly openedChats: URI[] = []; @@ -132,6 +146,7 @@ interface IChatCompositeBarHarness { readonly store: DisposableStore; readonly instantiationService: TestInstantiationService; readonly commandService: TestCommandService; + readonly contextMenuService: TestContextMenuService; readonly sessionsService: TestSessionsService; readonly editorGroupsService: TestEditorGroupsService; readonly bar: ChatCompositeBar; @@ -148,6 +163,7 @@ function createHarness(disposables: Pick, options?: { re const store = disposables.add(new DisposableStore()); const instantiationService = workbenchInstantiationService(undefined, store); const commandService = new TestCommandService(); + const contextMenuService = new TestContextMenuService(); const sessionsService = new TestSessionsService(); const editorGroupsService = store.add(new TestEditorGroupsService()); const mainChat = createChat('main', 'Main Chat'); @@ -160,6 +176,13 @@ function createHarness(disposables: Pick, options?: { re const showSessionActions = observableValue('test.showSessionActions', true); instantiationService.stub(ICommandService, commandService); + instantiationService.stub(IContextMenuService, contextMenuService); + const keybindingService = instantiationService.get(IKeybindingService); + const lookupKeybinding = keybindingService.lookupKeybinding.bind(keybindingService); + keybindingService.lookupKeybinding = commandId => commandId === RENAME_CHAT_COMMAND_ID + ? upcastPartial({ getLabel: () => 'F2' }) + : lookupKeybinding(commandId); + store.add({ dispose: () => keybindingService.lookupKeybinding = lookupKeybinding }); const closeAction = instantiationService.createInstance(MenuItemAction, { id: CLOSE_CHAT_COMMAND_ID, title: 'Close Chat' }, undefined, undefined, undefined, undefined); instantiationService.stub(IMenuService, new class extends mock() { override createMenu(): IMenu { @@ -196,7 +219,7 @@ function createHarness(disposables: Pick, options?: { re container.appendChild(bar.element); const tabs = Array.from(bar.element.querySelectorAll('.chat-composite-bar-tab')); - return { store, instantiationService, commandService, sessionsService, editorGroupsService, bar, container, session, tabs, chats, activeChatResource, visible, showSessionActions }; + return { store, instantiationService, commandService, contextMenuService, sessionsService, editorGroupsService, bar, container, session, tabs, chats, activeChatResource, visible, showSessionActions }; } suite('Sessions - ChatCompositeBar', () => { @@ -265,6 +288,38 @@ suite('Sessions - ChatCompositeBar', () => { assert.strictEqual(bar.element.querySelector('.chat-composite-bar-new-chat'), null); }); + test('uses the shared F2 rename command for chat tabs', async () => { + const { commandService, container, contextMenuService, session, tabs } = createHarness(disposables); + mainWindow.document.body.appendChild(container); + + try { + tabs[1].dispatchEvent(new MouseEvent(EventType.CONTEXT_MENU, { bubbles: true, cancelable: true, button: 2 })); + const renameAction = contextMenuService.delegate?.getActions().find(action => action.id === RENAME_CHAT_COMMAND_ID); + assert.ok(renameAction); + const keybinding = contextMenuService.delegate?.getKeyBinding?.(renameAction); + await renameAction.run(); + + assert.deepStrictEqual({ + label: renameAction.label, + keybinding: keybinding?.getLabel(), + commandCalls: commandService.calls, + }, { + label: 'Rename...', + keybinding: 'F2', + commandCalls: [{ + commandId: RENAME_CHAT_COMMAND_ID, + args: [{ + session, + chat: session.visibleChatTabs.get()[1], + inline: true, + }], + }], + }); + } finally { + container.remove(); + } + }); + test('matches the default and compact editor tab strip heights', () => { const { bar, container, editorGroupsService } = createHarness(disposables); mainWindow.document.body.appendChild(container);