diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts index 4ff78ffe937d8..12f597afba079 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts @@ -452,7 +452,7 @@ MenuRegistry.appendMenuItem(MenuId.ChatSessionsMenu, { group: 'inline', order: 2, when: ContextKeyExpr.and( - ChatContextKeys.isHistoryItem.isEqualTo(true), + ChatContextKeys.isArchivedItem.isEqualTo(true), ChatContextKeys.isActiveSession.isEqualTo(false) ) }); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts index f55e51dc7a168..1b50c9750dd2f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts @@ -20,7 +20,6 @@ export const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionE export type ChatSessionItemWithProvider = IChatSessionItem & { readonly provider: IChatSessionItemProvider; - isHistory?: boolean; relativeTime?: string; relativeTimeFullWord?: string; hideRelativeTime?: boolean; @@ -118,12 +117,14 @@ function applyTimeGrouping(sessions: ChatSessionItemWithProvider[]): void { } // Helper function to process session items with timestamps, sorting, and grouping -export function processSessionsWithTimeGrouping(sessions: ChatSessionItemWithProvider[]): void { +export function processSessionsWithTimeGrouping(sessions: ChatSessionItemWithProvider[]): ChatSessionItemWithProvider[] { + const sessionsTemp = [...sessions]; // Only process if we have sessions with timestamps if (sessions.some(session => session.timing?.startTime !== undefined)) { - sortSessionsByTimestamp(sessions); - applyTimeGrouping(sessions); + sortSessionsByTimestamp(sessionsTemp); + applyTimeGrouping(sessionsTemp); } + return sessionsTemp; } // Helper function to create context overlay for session items @@ -145,15 +146,15 @@ export function getSessionItemContextOverlay( } // Mark history items - overlay.push([ChatContextKeys.isHistoryItem.key, session.isHistory]); + overlay.push([ChatContextKeys.isArchivedItem.key, session.archived]); // Mark active sessions - check if session is currently open in editor or widget let isActiveSession = false; - if (!session.isHistory && provider?.chatSessionType === localChatSessionType) { + if (!session.archived && provider?.chatSessionType === localChatSessionType) { // Local non-history sessions are always active isActiveSession = true; - } else if (session.isHistory && chatWidgetService && chatService && editorGroupsService) { + } else if (session.archived && chatWidgetService && chatService && editorGroupsService) { // Check if session is open in a chat widget const widget = chatWidgetService.getWidgetBySessionResource(session.resource); if (widget) { diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts index 49d15b981891c..acac3a52c9241 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts @@ -16,6 +16,7 @@ import { IEditorGroup, IEditorGroupsService } from '../../../../services/editor/ import { IChatModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; +import { chatSessionResourceToId } from '../../common/chatUri.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatEditorInput } from '../chatEditorInput.js'; @@ -24,7 +25,6 @@ import { ChatSessionItemWithProvider, isChatSession } from './common.js'; export class LocalChatSessionsProvider extends Disposable implements IChatSessionItemProvider, IWorkbenchContribution { static readonly ID = 'workbench.contrib.localChatSessionsProvider'; static readonly CHAT_WIDGET_VIEW_ID = 'workbench.panel.chat.view.copilot'; - static readonly HISTORY_NODE_ID = 'show-history'; readonly chatSessionType = localChatSessionType; private readonly _onDidChange = this._register(new Emitter()); @@ -241,16 +241,30 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio } } }); + const history = await this.getHistoryItems(); + sessions.push(...history); + return sessions; + } - // TODO: This should not be a session items - const historyNode: IChatSessionItem = { - id: LocalChatSessionsProvider.HISTORY_NODE_ID, - resource: URI.parse(`${Schemas.vscodeLocalChatSession}://history`), - label: nls.localize('chat.sessions.showHistory', "History"), - timing: { startTime: 0 } - }; + private async getHistoryItems(): Promise { + try { + const allHistory = await this.chatService.getLocalSessionHistory(); + const historyItems = allHistory.map((historyDetail): ChatSessionItemWithProvider => ({ + id: chatSessionResourceToId(historyDetail.sessionResource), + resource: historyDetail.sessionResource, + label: historyDetail.title, + iconPath: Codicon.chatSparkle, + provider: this, + timing: { + startTime: historyDetail.lastMessageDate ?? Date.now() + }, + archived: true, + })); - // Add "Show history..." node at the end - return [...sessions, historyNode]; + return historyItems; + + } catch (error) { + return []; + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts index 7fc8b3409a77d..60ea52da9c84d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts @@ -42,14 +42,12 @@ import { IWorkbenchLayoutService, Position } from '../../../../../services/layou import { getLocalHistoryDateFormatter } from '../../../../localHistory/browser/localHistory.js'; import { IChatService } from '../../../common/chatService.js'; import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; -import { chatSessionResourceToId } from '../../../common/chatUri.js'; import { ChatConfiguration } from '../../../common/constants.js'; import { IChatWidgetService } from '../../chat.js'; import { allowedChatMarkdownHtmlTags } from '../../chatContentMarkdownRenderer.js'; import '../../media/chatSessions.css'; import { ChatSessionTracker } from '../chatSessionTracker.js'; import { ChatSessionItemWithProvider, extractTimestamp, getSessionItemContextOverlay, isLocalChatSessionItem, processSessionsWithTimeGrouping } from '../common.js'; -import { LocalChatSessionsProvider } from '../localChatSessionsProvider.js'; interface ISessionTemplateData { readonly container: HTMLElement; @@ -63,6 +61,25 @@ interface ISessionTemplateData { readonly customIcon: HTMLElement; } +export class ArchivedSessionItems { + private readonly items: Map = new Map(); + constructor(public readonly label: string) { + } + + pushItem(item: ChatSessionItemWithProvider): void { + const key = item.resource.toString(); + this.items.set(key, item); + } + + getItems(): ChatSessionItemWithProvider[] { + return Array.from(this.items.values()); + } + + clear(): void { + this.items.clear(); + } +} + export interface IGettingStartedItem { id: string; label: string; @@ -191,12 +208,26 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer, index: number, templateData: ISessionTemplateData): void { - const session = element.element as ChatSessionItemWithProvider; + if (element.element instanceof ArchivedSessionItems) { + this.renderArchivedNode(element.element, templateData); + return; + } + const session = element.element as ChatSessionItemWithProvider; // Add CSS class for local sessions let editableData: IEditableData | undefined; if (isLocalChatSessionItem(session)) { @@ -220,7 +251,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer { - +export class SessionsDataSource implements IAsyncDataSource { + // For now call it History until we support archive on all providers + private archivedItems = new ArchivedSessionItems(nls.localize('chat.sessions.archivedSessions', 'History')); constructor( private readonly provider: IChatSessionItemProvider, - private readonly chatService: IChatService, private readonly sessionTracker: ChatSessionTracker, ) { } - hasChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider): boolean { - const isProvider = element === this.provider; - if (isProvider) { + hasChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider | ArchivedSessionItems): boolean { + if (element === this.provider) { // Root provider always has children return true; } - // Check if this is the "Show history..." node - if ('id' in element && element.id === LocalChatSessionsProvider.HISTORY_NODE_ID) { - return true; + if (element instanceof ArchivedSessionItems) { + return element.getItems().length > 0; } return false; } - async getChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider): Promise { + async getChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider | ArchivedSessionItems): Promise<(ChatSessionItemWithProvider | ArchivedSessionItems)[]> { if (element === this.provider) { try { const items = await this.provider.provideChatSessionItems(CancellationToken.None); - const itemsWithProvider = items.map(item => { - const itemWithProvider: ChatSessionItemWithProvider = { ...item, provider: this.provider }; - - // Extract timestamp using the helper function - itemWithProvider.timing = { startTime: extractTimestamp(item) ?? 0 }; - + // Clear archived items from previous calls + this.archivedItems.clear(); + let ungroupedItems = items.map(item => { + const itemWithProvider = { ...item, provider: this.provider, timing: { startTime: extractTimestamp(item) ?? 0 } }; + if (itemWithProvider.archived) { + this.archivedItems.pushItem(itemWithProvider); + return; + } return itemWithProvider; - }); + }).filter(item => item !== undefined); - // Add hybrid local editor sessions for this provider using the centralized service + // Add hybrid local editor sessions for this provider if (this.provider.chatSessionType !== localChatSessionType) { const hybridSessions = await this.sessionTracker.getHybridSessionsForProvider(this.provider); const existingSessions = new ResourceSet(); - itemsWithProvider.forEach(s => existingSessions.add(s.resource)); - + // Iterate only over the ungrouped items, the only group we support for now is history + ungroupedItems.forEach(s => existingSessions.add(s.resource)); hybridSessions.forEach(session => { if (!existingSessions.has(session.resource)) { - itemsWithProvider.push(session as ChatSessionItemWithProvider); + ungroupedItems.push(session as ChatSessionItemWithProvider); existingSessions.add(session.resource); } }); - processSessionsWithTimeGrouping(itemsWithProvider); + ungroupedItems = processSessionsWithTimeGrouping(ungroupedItems); + } + + const result = []; + result.push(...ungroupedItems); + if (this.archivedItems.getItems().length > 0) { + result.push(this.archivedItems); } - return itemsWithProvider; + return result; } catch (error) { return []; } } - // Check if this is the "Show history..." node - if ('id' in element && element.id === LocalChatSessionsProvider.HISTORY_NODE_ID) { - return this.getHistoryItems(); + if (element instanceof ArchivedSessionItems) { + return processSessionsWithTimeGrouping(element.getItems()); } // Individual session items don't have children return []; } - - private async getHistoryItems(): Promise { - try { - // Get all chat history - const allHistory = await this.chatService.getLocalSessionHistory(); - - // Create history items with provider reference and timestamps - const historyItems = allHistory.map((historyDetail): ChatSessionItemWithProvider => ({ - id: chatSessionResourceToId(historyDetail.sessionResource), - resource: historyDetail.sessionResource, - label: historyDetail.title, - iconPath: Codicon.chatSparkle, - provider: this.provider, - timing: { - startTime: historyDetail.lastMessageDate ?? Date.now() - }, - isHistory: true, - })); - - // Apply sorting and time grouping - processSessionsWithTimeGrouping(historyItems); - - return historyItems; - - } catch (error) { - return []; - } - } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts index 17bea32f9d5fa..e2d16ade06b94 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts @@ -48,13 +48,17 @@ import { IChatEditorOptions } from '../../chatEditor.js'; import { ChatSessionTracker } from '../chatSessionTracker.js'; import { ChatSessionItemWithProvider, findExistingChatEditorByUri, getSessionItemContextOverlay, NEW_CHAT_SESSION_ACTION_ID } from '../common.js'; import { LocalChatSessionsProvider } from '../localChatSessionsProvider.js'; -import { GettingStartedDelegate, GettingStartedRenderer, IGettingStartedItem, SessionsDataSource, SessionsDelegate, SessionsRenderer } from './sessionsTreeRenderer.js'; +import { ArchivedSessionItems, GettingStartedDelegate, GettingStartedRenderer, IGettingStartedItem, SessionsDataSource, SessionsDelegate, SessionsRenderer } from './sessionsTreeRenderer.js'; // Identity provider for session items class SessionsIdentityProvider { - getId(element: ChatSessionItemWithProvider): string { + getId(element: ChatSessionItemWithProvider | ArchivedSessionItems): string { + if (element instanceof ArchivedSessionItems) { + return 'archived-session-items'; + } return element.resource.toString(); } + } // Accessibility provider for session items @@ -63,7 +67,7 @@ class SessionsAccessibilityProvider { return nls.localize('chatSessions', 'Chat Sessions'); } - getAriaLabel(element: ChatSessionItemWithProvider): string | null { + getAriaLabel(element: ChatSessionItemWithProvider | ArchivedSessionItems): string | null { return element.label; } } @@ -293,7 +297,7 @@ export class SessionsViewPane extends ViewPane { this.messageElement = append(container, $('.chat-sessions-message')); this.messageElement.style.display = 'none'; // Create the tree components - const dataSource = new SessionsDataSource(this.provider, this.chatService, this.sessionTracker); + const dataSource = new SessionsDataSource(this.provider, this.sessionTracker); const delegate = new SessionsDelegate(this.configurationService); const identityProvider = new SessionsIdentityProvider(); const accessibilityProvider = new SessionsAccessibilityProvider(); @@ -329,9 +333,6 @@ export class SessionsViewPane extends ViewPane { } }, getDragURI: (element: ChatSessionItemWithProvider) => { - if (element.id === LocalChatSessionsProvider.HISTORY_NODE_ID) { - return null; - } return getResourceForElement(element)?.toString() ?? null; }, getDragLabel: (elements: ChatSessionItemWithProvider[]) => { @@ -377,7 +378,10 @@ export class SessionsViewPane extends ViewPane { // Register context menu event for right-click actions this._register(this.tree.onContextMenu((e) => { - if (e.element && e.element.id !== LocalChatSessionsProvider.HISTORY_NODE_ID) { + if (e.element && !(e.element instanceof ArchivedSessionItems)) { + this.showContextMenu(e); + } + if (e.element) { this.showContextMenu(e); } })); @@ -473,9 +477,7 @@ export class SessionsViewPane extends ViewPane { if (this.chatWidgetService.getWidgetBySessionResource(session.resource)) { return; } - - if (session.id === LocalChatSessionsProvider.HISTORY_NODE_ID) { - // Don't try to open the "Show history..." node itself + if (session instanceof ArchivedSessionItems) { return; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index f51121a4bf678..031a4537e145b 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -92,7 +92,7 @@ export namespace ChatContextKeys { export const inEmptyStateWithHistoryEnabled = new RawContextKey('chatInEmptyStateWithHistoryEnabled', false, { type: 'boolean', description: localize('chatInEmptyStateWithHistoryEnabled', "True when chat empty state history is enabled AND chat is in empty state.") }); export const sessionType = new RawContextKey('chatSessionType', '', { type: 'string', description: localize('chatSessionType', "The type of the current chat session item.") }); - export const isHistoryItem = new RawContextKey('chatIsHistoryItem', false, { type: 'boolean', description: localize('chatIsHistoryItem', "True when the chat session item is from history.") }); + export const isArchivedItem = new RawContextKey('chatIsArchivedItem', false, { type: 'boolean', description: localize('chatIsArchivedItem', "True when the chat session item is archived.") }); export const isActiveSession = new RawContextKey('chatIsActiveSession', false, { type: 'boolean', description: localize('chatIsActiveSession', "True when the chat session is currently active (not deletable).") }); export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index b0b720d1e09bb..6c54c7131d8f2 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -76,7 +76,7 @@ export interface IChatSessionItem { insertions: number; deletions: number; }; - + archived?: boolean; } export type IChatSessionHistoryItem = {