Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
});
Expand Down
15 changes: 8 additions & 7 deletions src/vs/workbench/contrib/chat/browser/chatSessions/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<void>());
Expand Down Expand Up @@ -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<ChatSessionItemWithProvider[]> {
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 [];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -63,6 +61,25 @@ interface ISessionTemplateData {
readonly customIcon: HTMLElement;
}

export class ArchivedSessionItems {
private readonly items: Map<string, ChatSessionItemWithProvider> = 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;
Expand Down Expand Up @@ -191,12 +208,26 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
default:
return Codicon.circleOutline;
}
}

private renderArchivedNode(node: ArchivedSessionItems, templateData: ISessionTemplateData): void {
templateData.customIcon.className = '';
templateData.descriptionRow.style.display = 'none';
templateData.timestamp.parentElement!.style.display = 'none';

const childCount = node.getItems().length;
templateData.iconLabel.setLabel(node.label, undefined, {
title: childCount === 1 ? nls.localize('chat.sessions.groupNode.single', '1 session') : nls.localize('chat.sessions.groupNode.multiple', '{0} sessions', childCount)
});
}

renderElement(element: ITreeNode<IChatSessionItem, FuzzyScore>, 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)) {
Expand All @@ -220,7 +251,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS

// Handle different icon types
let iconTheme: ThemeIcon | undefined;
if (!session.iconPath && session.id !== LocalChatSessionsProvider.HISTORY_NODE_ID) {
if (!session.iconPath) {
iconTheme = this.statusToIcon(session.status);
} else {
iconTheme = session.iconPath;
Expand Down Expand Up @@ -510,100 +541,77 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
}

// Chat sessions item data source for the tree
export class SessionsDataSource implements IAsyncDataSource<IChatSessionItemProvider, ChatSessionItemWithProvider> {

export class SessionsDataSource implements IAsyncDataSource<IChatSessionItemProvider, ChatSessionItemWithProvider | ArchivedSessionItems> {
// 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<ChatSessionItemWithProvider[]> {
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<ChatSessionItemWithProvider[]> {
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 [];
}
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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[]) => {
Expand Down Expand Up @@ -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);
}
}));
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/common/chatContextKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export namespace ChatContextKeys {
export const inEmptyStateWithHistoryEnabled = new RawContextKey<boolean>('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<string>('chatSessionType', '', { type: 'string', description: localize('chatSessionType', "The type of the current chat session item.") });
export const isHistoryItem = new RawContextKey<boolean>('chatIsHistoryItem', false, { type: 'boolean', description: localize('chatIsHistoryItem', "True when the chat session item is from history.") });
export const isArchivedItem = new RawContextKey<boolean>('chatIsArchivedItem', false, { type: 'boolean', description: localize('chatIsArchivedItem', "True when the chat session item is archived.") });
export const isActiveSession = new RawContextKey<boolean>('chatIsActiveSession', false, { type: 'boolean', description: localize('chatIsActiveSession', "True when the chat session is currently active (not deletable).") });
export const isKatexMathElement = new RawContextKey<boolean>('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") });
}
Expand Down
Loading
Loading