Skip to content
Closed
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
46 changes: 43 additions & 3 deletions src/vs/workbench/contrib/chat/browser/actions/chatActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const CHAT_CATEGORY = localize2('chat.category', 'Chat');
export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`;
export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`;
export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open';
export const CHAT_OPEN_HISTORICAL_SESSION_ACTION_ID = 'workbench.action.chat.openHistoricalSession';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const CHAT_OPEN_HISTORICAL_SESSION_ACTION_ID = 'workbench.action.chat.openHistoricalSession';
export const CHAT_RESTORE_SESSION_ACTION_ID = 'workbench.action.chat.restoreSession';

export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup';
const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle';
const CHAT_CLEAR_HISTORY_ACTION_ID = 'workbench.action.chat.clearHistory';
Expand Down Expand Up @@ -579,7 +580,7 @@ export function registerChatActions() {
separator,
{
label: i.title,
description: i.isActive ? `(${localize('currentChatLabel', 'current')})` : '',
description: i.isActive ? `(${i.createdOnBranch} - ${localize('currentChatLabel', 'current')})` : `${i.createdOnBranch}`,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use last active branch instead of createdOnBranch?

chat: i,
buttons: i.isActive ? [renameButton] : [
renameButton,
Expand Down Expand Up @@ -1232,7 +1233,7 @@ export function registerChatActions() {
const suggestCtrl = SuggestController.get(widget.inputEditor);
if (suggestCtrl) {
const curText = widget.inputEditor.getValue();
const newValue = curText ? `@ ${curText}` : '@';
const newValue = curText ? `@${curText}` : '@';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert. this seems unrelated.

if (!curText.startsWith('@')) {
widget.inputEditor.setValue(newValue);
}
Expand Down Expand Up @@ -1402,7 +1403,7 @@ export function registerChatActions() {

override async run(accessor: ServicesAccessor): Promise<void> {
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
extensionsWorkbenchService.openSearch(`@feature:${CopilotUsageExtensionFeatureId}`);
extensionsWorkbenchService.openSearch(`@feature: ${CopilotUsageExtensionFeatureId}`);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert. unrelated.

}
});

Expand Down Expand Up @@ -1984,3 +1985,42 @@ registerAction2(class EditToolApproval extends Action2 {
}
}
});

registerAction2(class OpenHistoricalChatSessionAction extends Action2 {
constructor() {
super({
id: CHAT_OPEN_HISTORICAL_SESSION_ACTION_ID,
title: localize2('openHistoricalChatSession', "Open Historical Chat Session"),
category: CHAT_CATEGORY,
f1: true,
precondition: ChatContextKeys.enabled
});
}

async run(accessor: ServicesAccessor, options?: { sessionId?: string }): Promise<void> {
const viewsService = accessor.get(IViewsService);
const editorService = accessor.get(IEditorService);

if (!options?.sessionId) {
console.log('No sessionId provided to openHistoricalSession command');
return;
}

try {
const sessionId = options.sessionId;
console.log(`Opening historical chat session with sessionId: ${sessionId}`);

// Try to open in chat view first
const view = await viewsService.openView<ChatViewPane>(ChatViewId);
if (view) {
await view.loadSession(sessionId);
} else {
// Fallback to opening in editor
const options: IChatEditorOptions = { target: { sessionId }, pinned: true };
await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, ACTIVE_GROUP);
}
} catch (error) {
console.error('Failed to open historical chat session:', error);
}
}
});
19 changes: 18 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,10 @@ export interface ISerializableChatData3 extends Omit<ISerializableChatData2, 've
version: 3;
customTitle: string | undefined;
inputType?: string;
/** Git branch name when the session was created (best effort). */
createdOnBranch?: string;
/** Git branch name when the session was last activated/used (best effort). */
lastUsedOnBranch?: string;
}

/**
Expand Down Expand Up @@ -1368,6 +1372,11 @@ export class ChatModel extends Disposable implements IChatModel {
return this._creationDate;
}

/** Git branch name when this session was created (best effort, persisted). */
public createdOnBranch: string | undefined;
/** Git branch name when this session was last activated (best effort, persisted). */
public lastUsedOnBranch: string | undefined;

private _lastMessageDate: number;
get lastMessageDate(): number {
return this._lastMessageDate;
Expand Down Expand Up @@ -1455,6 +1464,12 @@ export class ChatModel extends Disposable implements IChatModel {
this._lastMessageDate = (isValid && initialData.lastMessageDate) || this._creationDate;
this._customTitle = isValid ? initialData.customTitle : undefined;

// Rehydrate branch metadata if present
if (isValid) {
this.createdOnBranch = initialData.createdOnBranch;
this.lastUsedOnBranch = initialData.lastUsedOnBranch;
}

this._initialRequesterUsername = initialData?.requesterUsername;
this._initialResponderUsername = initialData?.responderUsername;
this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri);
Expand Down Expand Up @@ -1884,7 +1899,9 @@ export class ChatModel extends Disposable implements IChatModel {
creationDate: this._creationDate,
isImported: this._isImported,
lastMessageDate: this._lastMessageDate,
customTitle: this._customTitle
customTitle: this._customTitle,
createdOnBranch: this.createdOnBranch,
lastUsedOnBranch: this.lastUsedOnBranch,
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/contrib/chat/common/chatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,10 @@ export interface IChatDetail {
title: string;
lastMessageDate: number;
isActive: boolean;
/** Git branch when session was created (if recorded). */
createdOnBranch?: string;
/** Git branch when session was last used (if recorded). */
lastUsedOnBranch?: string;
}

export interface IChatProviderInfo {
Expand Down
48 changes: 47 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { StopWatch } from '../../../../base/common/stopwatch.js';
import { URI } from '../../../../base/common/uri.js';
import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
import { localize } from '../../../../nls.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../platform/log/common/log.js';
Expand All @@ -33,6 +34,7 @@ import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChat
import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js';
import { IChatSessionsService } from './chatSessionsService.js';
import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js';
import { IGitStatus } from './gitStatusService.js';
import { IChatSlashCommandService } from './chatSlashCommands.js';
import { IChatTransferService } from './chatTransferService.js';
import { ChatSessionUri } from './chatUri.js';
Expand Down Expand Up @@ -118,6 +120,8 @@ export class ChatService extends Disposable implements IChatService {
@IChatTransferService private readonly chatTransferService: IChatTransferService,
@IChatSessionsService private readonly chatSessionService: IChatSessionsService,
@IMcpService private readonly mcpService: IMcpService,
@IGitStatus private readonly gitStatus: IGitStatus,
@ICommandService private readonly commandService: ICommandService,
) {
super();

Expand Down Expand Up @@ -160,6 +164,18 @@ export class ChatService extends Disposable implements IChatService {
const models = this._sessionModels.observable.read(reader).values();
return Array.from(models).some(model => model.requestInProgressObs.read(reader));
});

// Listen for branch changes
this._register(this.gitStatus.onChangedBranch(async branch => {
console.log('Branch changed to:', branch);
const history = await this.getHistory();
const sessionId = history.find(h => h.lastUsedOnBranch === branch)?.sessionId;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check ordering on this. Ensure history is sorted in an expected way. Ideally, we want the most recent session related to the branch.

if (sessionId) {
this.commandService.executeCommand('workbench.action.chat.openHistoricalSession', { sessionId });
} else {
this.commandService.executeCommand('workbench.action.chat.newChat');
}
}));
}

isEnabled(location: ChatAgentLocation): boolean {
Expand Down Expand Up @@ -303,6 +319,8 @@ export class ChatService extends Disposable implements IChatService {
title,
lastMessageDate: session.lastMessageDate,
isActive: true,
createdOnBranch: session.createdOnBranch,
lastUsedOnBranch: session.lastUsedOnBranch,
} satisfies IChatDetail;
});

Expand Down Expand Up @@ -331,12 +349,22 @@ export class ChatService extends Disposable implements IChatService {

private _startSession(someSessionHistory: IExportableChatData | ISerializableChatData | undefined, location: ChatAgentLocation, isGlobalEditingSession: boolean, token: CancellationToken, inputType?: string): ChatModel {
const model = this.instantiationService.createInstance(ChatModel, someSessionHistory, { initialLocation: location, inputType });
// Initialize branch metadata on creation
const branch = this.gitStatus.getCurrentBranch();
if (branch) {
if (!model.createdOnBranch) {
model.createdOnBranch = branch;
}
model.lastUsedOnBranch = branch;
}
if (location === ChatAgentLocation.Chat) {
model.startEditingSession(isGlobalEditingSession);
}

this._sessionModels.set(model.sessionId, model);
this.initializeSession(model, token);
// Persist immediately to capture branch info
this._chatSessionStore.storeSessions([model]);
return model;
}

Expand All @@ -349,6 +377,19 @@ export class ChatService extends Disposable implements IChatService {
this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e));
}

/**
* Update session metadata to reflect that it has been actively used (selected, restored, or loaded).
* Captures the current branch (best effort) for lastUsedOnBranch and persists the session metadata.
*/
private _markSessionUsed(model: ChatModel): void {
const branch = this.gitStatus.getCurrentBranch();
if (branch) {
model.lastUsedOnBranch = branch;
}
// Persist metadata update only (non-blocking)
this._chatSessionStore.storeSessions([model]);
}

async activateDefaultAgent(location: ChatAgentLocation): Promise<void> {
await this.extensionService.whenInstalledExtensionsRegistered();

Expand Down Expand Up @@ -382,6 +423,7 @@ export class ChatService extends Disposable implements IChatService {
this.trace('getOrRestoreSession', `sessionId: ${sessionId}`);
const model = this._sessionModels.get(sessionId);
if (model) {
this._markSessionUsed(model);
return model;
}

Expand All @@ -403,6 +445,7 @@ export class ChatService extends Disposable implements IChatService {
this._transferredSessionData = undefined;
}

this._markSessionUsed(session);
return session;
}

Expand Down Expand Up @@ -443,7 +486,9 @@ export class ChatService extends Disposable implements IChatService {
}

loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined {
return this._startSession(data, data.initialLocation ?? ChatAgentLocation.Chat, true, CancellationToken.None);
const model = this._startSession(data, data.initialLocation ?? ChatAgentLocation.Chat, true, CancellationToken.None);
this._markSessionUsed(model);
return model;
}

async loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModel | undefined> {
Expand All @@ -466,6 +511,7 @@ export class ChatService extends Disposable implements IChatService {
const content = await this.chatSessionService.provideChatSessionContent(chatSessionType, parsed.sessionId, CancellationToken.None);

const model = this._startSession(undefined, location, true, CancellationToken.None, chatSessionType);
this._markSessionUsed(model);
if (!this._contentProviderSessionModels.has(chatSessionType)) {
this._contentProviderSessionModels.set(chatSessionType, new Map());
}
Expand Down
13 changes: 10 additions & 3 deletions src/vs/workbench/contrib/chat/common/chatSessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class ChatSessionStore extends Disposable {
await this.fileService.writeFile(storageLocation, VSBuffer.fromString(content));

// Write succeeded, update index
index.entries[session.sessionId] = getSessionMetadata(session);
index.entries[session.sessionId] = getSessionMetadata(session, index.entries[session.sessionId]);
} catch (e) {
this.reportError('sessionWrite', 'Error writing chat session', e);
}
Expand Down Expand Up @@ -381,6 +381,7 @@ export class ChatSessionStore extends Disposable {
public getChatStorageFolder(): URI {
return this.storageRoot;
}

}

interface IChatSessionEntryMetadata {
Expand All @@ -389,6 +390,10 @@ interface IChatSessionEntryMetadata {
lastMessageDate: number;
isImported?: boolean;
initialLocation?: ChatAgentLocation;
/** Git branch name at time the session was first persisted (best effort). */
createdOnBranch?: string;
/** Git branch name at time the session was last written (best effort). */
lastUsedOnBranch?: string;

/**
* This only exists because the migrated data from the storage service had empty sessions persisted, and it's impossible to know which ones are
Expand Down Expand Up @@ -440,7 +445,7 @@ function isChatSessionIndex(data: unknown): data is IChatSessionIndexData {
return true;
}

function getSessionMetadata(session: ChatModel | ISerializableChatData): IChatSessionEntryMetadata {
function getSessionMetadata(session: ChatModel | ISerializableChatData, existingMetadata: IChatSessionEntryMetadata | undefined): IChatSessionEntryMetadata {
const title = session instanceof ChatModel ?
session.customTitle || (session.getRequests().length > 0 ? ChatModel.getDefaultTitle(session.getRequests()) : '') :
session.customTitle ?? (session.requests.length > 0 ? ChatModel.getDefaultTitle(session.requests) : '');
Expand All @@ -451,7 +456,9 @@ function getSessionMetadata(session: ChatModel | ISerializableChatData): IChatSe
lastMessageDate: session.lastMessageDate,
isImported: session.isImported,
initialLocation: session.initialLocation,
isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0
isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0,
createdOnBranch: session instanceof ChatModel ? (session.createdOnBranch ?? existingMetadata?.createdOnBranch) : (session as ISerializableChatData).createdOnBranch ?? existingMetadata?.createdOnBranch,
lastUsedOnBranch: session instanceof ChatModel ? (session.lastUsedOnBranch ?? existingMetadata?.lastUsedOnBranch) : (session as ISerializableChatData).lastUsedOnBranch ?? existingMetadata?.lastUsedOnBranch,
};
}

Expand Down
Loading
Loading