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 @@ -20,7 +20,7 @@ import { Registry } from '../../../../../platform/registry/common/platform.js';
import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';
import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { ArchiveAgentSessionAction, UnarchiveAgentSessionAction, RefreshAgentSessionsViewAction, FindAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction } from './agentSessionsActions.js';
import { ArchiveAgentSessionAction, UnarchiveAgentSessionAction, RefreshAgentSessionsViewAction, FindAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction } from './agentSessionsActions.js';

//#region View Container and View Registration

Expand Down Expand Up @@ -65,6 +65,8 @@ Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([agentSe

registerAction2(ArchiveAgentSessionAction);
registerAction2(UnarchiveAgentSessionAction);
registerAction2(MarkAgentSessionUnreadAction);
registerAction2(MarkAgentSessionReadAction);
registerAction2(OpenAgentSessionInNewWindowAction);
registerAction2(OpenAgentSessionInEditorGroupAction);
registerAction2(OpenAgentSessionInNewEditorGroupAction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ import { ACTIVE_GROUP, AUX_WINDOW_GROUP, PreferredGroup, SIDE_GROUP } from '../.
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
import { getPartByLocation } from '../../../../services/views/browser/viewsService.js';
import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';
import { IAgentSessionsService } from './agentSessionsService.js';

//#region Session Title Actions

export class ArchiveAgentSessionAction extends Action2 {

constructor() {
super({
id: 'agentSession.archive',
Expand All @@ -44,12 +46,14 @@ export class ArchiveAgentSessionAction extends Action2 {
}
});
}

run(accessor: ServicesAccessor, session: IAgentSession): void {
session.setArchived(true);
}
}

export class UnarchiveAgentSessionAction extends Action2 {

constructor() {
super({
id: 'agentSession.unarchive',
Expand All @@ -63,6 +67,7 @@ export class UnarchiveAgentSessionAction extends Action2 {
}
});
}

run(accessor: ServicesAccessor, session: IAgentSession): void {
session.setArchived(false);
}
Expand Down Expand Up @@ -274,11 +279,64 @@ export class OpenAgentSessionInNewWindowAction extends BaseOpenAgentSessionActio
}
}

export class MarkAgentSessionUnreadAction extends Action2 {

constructor() {
super({
id: 'agentSession.markUnread',
title: localize2('markUnread', "Mark as Unread"),
menu: {
id: MenuId.AgentSessionsContext,
group: 'edit',
order: 1,
when: ChatContextKeys.isReadAgentSession,
}
});
}

run(accessor: ServicesAccessor, context?: IMarshalledChatSessionContext): void {
const agentSessionsService = accessor.get(IAgentSessionsService);

if (!context) {
return;
}

agentSessionsService.getSession(context.session.resource)?.setRead(false);
}
}

export class MarkAgentSessionReadAction extends Action2 {

constructor() {
super({
id: 'agentSession.markRead',
title: localize2('markRead', "Mark as Read"),
menu: {
id: MenuId.AgentSessionsContext,
group: 'edit',
order: 1,
when: ChatContextKeys.isReadAgentSession.negate(),
}
});
}

run(accessor: ServicesAccessor, context?: IMarshalledChatSessionContext): void {
const agentSessionsService = accessor.get(IAgentSessionsService);

if (!context) {
return;
}

agentSessionsService.getSession(context.session.resource)?.setRead(true);
}
}

//#endregion

//#region View Actions

export class RefreshAgentSessionsViewAction extends ViewAction<AgentSessionsView> {

constructor() {
super({
id: 'agentSessionsView.refresh',
Expand All @@ -292,12 +350,14 @@ export class RefreshAgentSessionsViewAction extends ViewAction<AgentSessionsView
viewId: AGENT_SESSIONS_VIEW_ID
});
}

runInView(accessor: ServicesAccessor, view: AgentSessionsView): void {
view.refresh();
}
}

export class FindAgentSessionAction extends ViewAction<AgentSessionsView> {

constructor() {
super({
id: 'agentSessionsView.find',
Expand All @@ -311,6 +371,7 @@ export class FindAgentSessionAction extends ViewAction<AgentSessionsView> {
viewId: AGENT_SESSIONS_VIEW_ID
});
}

runInView(accessor: ServicesAccessor, view: AgentSessionsView): void {
view.openFind();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ import { Event } from '../../../../../base/common/event.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ITreeContextMenuEvent } from '../../../../../base/browser/ui/tree/tree.js';
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
import { getFlatActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { Separator } from '../../../../../base/common/actions.js';
import { IChatService } from '../../common/chatService.js';
import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js';
import { TreeFindMode } from '../../../../../base/browser/ui/tree/abstractTree.js';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
import { IMarshalledChatSessionContext } from '../actions/chatSessionActions.js';
import { distinct } from '../../../../../base/common/arrays.js';
import { IAgentSessionsService } from './agentSessionsService.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
import { IListStyles } from '../../../../../base/browser/ui/list/listWidget.js';
Expand Down Expand Up @@ -184,6 +183,8 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
providerType: session.providerType
});

session.setRead(true); // mark as read when opened

let sessionOptions: IChatEditorOptions;
if (isLocalAgentSessionItem(session)) {
sessionOptions = {};
Expand Down Expand Up @@ -223,11 +224,13 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
const provider = await this.chatSessionsService.activateChatSessionItemProvider(session.providerType);
const contextOverlay = getSessionItemContextOverlay(session, provider, this.chatService, this.editorGroupsService);
contextOverlay.push([ChatContextKeys.isCombinedAgentSessionsViewer.key, true]);
contextOverlay.push([ChatContextKeys.isReadAgentSession.key, session.isRead()]);
contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, session.isArchived()]);
const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay));

const marshalledSession: IMarshalledChatSessionContext = { session, $mid: MarshalledId.ChatSessionContext };
this.contextMenuService.showContextMenu({
getActions: () => distinct(getFlatActionBarActions(menu.getActions({ arg: marshalledSession, shouldForwardArgs: true })), action => action.id),
getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)),
getAnchor: () => anchor,
getActionsContext: () => marshalledSession,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export function getAgentChangesSummary(changes: IAgentSession['changes']) {
export interface IAgentSession extends IAgentSessionData {
isArchived(): boolean;
setArchived(archived: boolean): void;

isRead(): boolean;
setRead(read: boolean): void;
}

interface IInternalAgentSessionData extends IAgentSessionData {
Expand Down Expand Up @@ -118,6 +121,11 @@ export function isAgentSessionsModel(obj: IAgentSessionsModel | IAgentSession):
return Array.isArray(sessionsModel?.sessions);
}

interface IAgentSessionState {
readonly archived: boolean;
readonly read: number /* last date turned read */;
}

//#endregion

export class AgentSessionsModel extends Disposable implements IAgentSessionsModel {
Expand Down Expand Up @@ -341,13 +349,15 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
return {
...data,
isArchived: () => this.isArchived(data),
setArchived: (archived: boolean) => this.setArchived(data, archived)
setArchived: (archived: boolean) => this.setArchived(data, archived),
isRead: () => this.isRead(data),
setRead: (read: boolean) => this.setRead(data, read),
};
}

//#region States

private readonly sessionStates: ResourceMap<{ archived: boolean }>;
private readonly sessionStates: ResourceMap<IAgentSessionState>;

private isArchived(session: IInternalAgentSessionData): boolean {
return this.sessionStates.get(session.resource)?.archived ?? Boolean(session.archived);
Expand All @@ -358,7 +368,25 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
return; // no change
}

this.sessionStates.set(session.resource, { archived });
const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 };
this.sessionStates.set(session.resource, { ...state, archived });

this._onDidChangeSessions.fire();
}

private isRead(session: IInternalAgentSessionData): boolean {
const readDate = this.sessionStates.get(session.resource)?.read;

return (readDate ?? 0) >= (session.timing.endTime ?? session.timing.startTime);
}

private setRead(session: IInternalAgentSessionData, read: boolean): void {
if (read === this.isRead(session)) {
return; // no change
}

const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 };
this.sessionStates.set(session.resource, { ...state, read: read ? Date.now() : 0 });

this._onDidChangeSessions.fire();
}
Expand Down Expand Up @@ -397,9 +425,8 @@ interface ISerializedAgentSession {
};
}

interface ISerializedAgentSessionState {
interface ISerializedAgentSessionState extends IAgentSessionState {
readonly resource: UriComponents;
readonly archived: boolean;
}

class AgentSessionsCache {
Expand Down Expand Up @@ -482,17 +509,18 @@ class AgentSessionsCache {

//#region States

saveSessionStates(states: ResourceMap<{ archived: boolean }>): void {
saveSessionStates(states: ResourceMap<IAgentSessionState>): void {
const serialized: ISerializedAgentSessionState[] = Array.from(states.entries()).map(([resource, state]) => ({
resource: resource.toJSON(),
archived: state.archived
archived: state.archived,
read: state.read
}));

this.storageService.store(AgentSessionsCache.STATE_STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE);
}

loadSessionStates(): ResourceMap<{ archived: boolean }> {
const states = new ResourceMap<{ archived: boolean }>();
loadSessionStates(): ResourceMap<IAgentSessionState> {
const states = new ResourceMap<IAgentSessionState>();

const statesCache = this.storageService.get(AgentSessionsCache.STATE_STORAGE_KEY, StorageScope.WORKSPACE);
if (!statesCache) {
Expand All @@ -503,7 +531,10 @@ class AgentSessionsCache {
const cached = JSON.parse(statesCache) as ISerializedAgentSessionState[];

for (const entry of cached) {
states.set(URI.revive(entry.resource), { archived: entry.archived });
states.set(URI.revive(entry.resource), {
archived: entry.archived,
read: entry.read
});
}
} catch {
// invalid data in storage, fallback to empty states
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes

// Title Actions - Update context keys
ChatContextKeys.isArchivedAgentSession.bindTo(template.contextKeyService).set(session.element.isArchived());
ChatContextKeys.isReadAgentSession.bindTo(template.contextKeyService).set(session.element.isRead());
template.titleToolbar.context = session.element;

// Details Actions
Expand Down Expand Up @@ -201,7 +202,11 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
return Codicon.error;
}

return Codicon.pass;
if (!session.isRead() && !session.isArchived()) {
return Codicon.circleFilled;
}

return Codicon.blank;
}

private renderDescription(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@
.agent-session-icon {
flex-shrink: 0;
font-size: 16px;

&.codicon.codicon-session-in-progress {
color: var(--vscode-textLink-foreground);
}

&.codicon.codicon-error {
color: var(--vscode-errorForeground);
}

&.codicon.codicon-circle-filled {
color: var(--vscode-textLink-foreground);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class ChatSessionsViewContrib extends Disposable implements IWorkbenchCon
const providersWithDisplayNames = otherProviders.map(provider => {
const extContribution = extensionPointContributions.find(c => c.type === provider.chatSessionType);
if (!extContribution) {
this.logService.warn(`No extension contribution found for chat session type: ${provider.chatSessionType}`);
this.logService.trace(`No extension contribution found for chat session type: ${provider.chatSessionType}`);
return null;
}
return {
Expand Down
7 changes: 4 additions & 3 deletions src/vs/workbench/contrib/chat/common/chatContextKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ export namespace ChatContextKeys {
export const agentSessionsViewerOrientation = new RawContextKey<number>('agentSessionsViewerOrientation', undefined, { type: 'number', description: localize('agentSessionsViewerOrientation', "Orientation of the agent sessions view in the chat view.") });
export const agentSessionsViewerPosition = new RawContextKey<number>('agentSessionsViewerPosition', undefined, { type: 'number', description: localize('agentSessionsViewerPosition', "Position of the agent sessions view in the chat view.") });
export const agentSessionType = new RawContextKey<string>('chatSessionType', '', { type: 'string', description: localize('agentSessionType', "The type of the current agent session item.") });
export const hasAgentSessionChanges = new RawContextKey<boolean>('chatSessionHasAgentChanges', false, { type: 'boolean', description: localize('chatSessionHasAgentChanges', "True when the current agent session item has changes.") });
export const isArchivedAgentSession = new RawContextKey<boolean>('agentIsArchived', false, { type: 'boolean', description: localize('agentIsArchived', "True when the agent session item is archived.") });
export const isActiveAgentSession = new RawContextKey<boolean>('agentIsActive', false, { type: 'boolean', description: localize('agentIsActive', "True when the agent session is currently active (not deletable).") });
export const hasAgentSessionChanges = new RawContextKey<boolean>('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") });
export const isArchivedAgentSession = new RawContextKey<boolean>('agentSessionIsArchived', false, { type: 'boolean', description: localize('agentSessionIsArchived', "True when the agent session item is archived.") });
export const isReadAgentSession = new RawContextKey<boolean>('agentSessionIsRead', false, { type: 'boolean', description: localize('agentSessionIsRead', "True when the agent session item is read.") });
export const isActiveAgentSession = new RawContextKey<boolean>('agentSessionIsActive', false, { type: 'boolean', description: localize('agentSessionIsActive', "True when the agent 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