Skip to content

Commit defdb1b

Browse files
committed
Update session card UX from latest designs (fix #281754) (#281759)
1 parent 8824832 commit defdb1b

File tree

9 files changed

+347
-23
lines changed

9 files changed

+347
-23
lines changed

src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { Registry } from '../../../../../platform/registry/common/platform.js';
2020
import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js';
2121
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';
2222
import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
23-
import { ArchiveAgentSessionAction, UnarchiveAgentSessionAction, RefreshAgentSessionsViewAction, FindAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction } from './agentSessionsActions.js';
23+
import { ArchiveAgentSessionAction, UnarchiveAgentSessionAction, RefreshAgentSessionsViewAction, FindAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction } from './agentSessionsActions.js';
2424

2525
//#region View Container and View Registration
2626

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

6666
registerAction2(ArchiveAgentSessionAction);
6767
registerAction2(UnarchiveAgentSessionAction);
68+
registerAction2(MarkAgentSessionUnreadAction);
69+
registerAction2(MarkAgentSessionReadAction);
6870
registerAction2(OpenAgentSessionInNewWindowAction);
6971
registerAction2(OpenAgentSessionInEditorGroupAction);
7072
registerAction2(OpenAgentSessionInNewEditorGroupAction);

src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ import { ACTIVE_GROUP, AUX_WINDOW_GROUP, PreferredGroup, SIDE_GROUP } from '../.
2727
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
2828
import { getPartByLocation } from '../../../../services/views/browser/viewsService.js';
2929
import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';
30+
import { IAgentSessionsService } from './agentSessionsService.js';
3031

3132
//#region Session Title Actions
3233

3334
export class ArchiveAgentSessionAction extends Action2 {
35+
3436
constructor() {
3537
super({
3638
id: 'agentSession.archive',
@@ -44,12 +46,14 @@ export class ArchiveAgentSessionAction extends Action2 {
4446
}
4547
});
4648
}
49+
4750
run(accessor: ServicesAccessor, session: IAgentSession): void {
4851
session.setArchived(true);
4952
}
5053
}
5154

5255
export class UnarchiveAgentSessionAction extends Action2 {
56+
5357
constructor() {
5458
super({
5559
id: 'agentSession.unarchive',
@@ -63,6 +67,7 @@ export class UnarchiveAgentSessionAction extends Action2 {
6367
}
6468
});
6569
}
70+
6671
run(accessor: ServicesAccessor, session: IAgentSession): void {
6772
session.setArchived(false);
6873
}
@@ -274,11 +279,64 @@ export class OpenAgentSessionInNewWindowAction extends BaseOpenAgentSessionActio
274279
}
275280
}
276281

282+
export class MarkAgentSessionUnreadAction extends Action2 {
283+
284+
constructor() {
285+
super({
286+
id: 'agentSession.markUnread',
287+
title: localize2('markUnread', "Mark as Unread"),
288+
menu: {
289+
id: MenuId.AgentSessionsContext,
290+
group: 'edit',
291+
order: 1,
292+
when: ChatContextKeys.isReadAgentSession,
293+
}
294+
});
295+
}
296+
297+
run(accessor: ServicesAccessor, context?: IMarshalledChatSessionContext): void {
298+
const agentSessionsService = accessor.get(IAgentSessionsService);
299+
300+
if (!context) {
301+
return;
302+
}
303+
304+
agentSessionsService.getSession(context.session.resource)?.setRead(false);
305+
}
306+
}
307+
308+
export class MarkAgentSessionReadAction extends Action2 {
309+
310+
constructor() {
311+
super({
312+
id: 'agentSession.markRead',
313+
title: localize2('markRead', "Mark as Read"),
314+
menu: {
315+
id: MenuId.AgentSessionsContext,
316+
group: 'edit',
317+
order: 1,
318+
when: ChatContextKeys.isReadAgentSession.negate(),
319+
}
320+
});
321+
}
322+
323+
run(accessor: ServicesAccessor, context?: IMarshalledChatSessionContext): void {
324+
const agentSessionsService = accessor.get(IAgentSessionsService);
325+
326+
if (!context) {
327+
return;
328+
}
329+
330+
agentSessionsService.getSession(context.session.resource)?.setRead(true);
331+
}
332+
}
333+
277334
//#endregion
278335

279336
//#region View Actions
280337

281338
export class RefreshAgentSessionsViewAction extends ViewAction<AgentSessionsView> {
339+
282340
constructor() {
283341
super({
284342
id: 'agentSessionsView.refresh',
@@ -292,12 +350,14 @@ export class RefreshAgentSessionsViewAction extends ViewAction<AgentSessionsView
292350
viewId: AGENT_SESSIONS_VIEW_ID
293351
});
294352
}
353+
295354
runInView(accessor: ServicesAccessor, view: AgentSessionsView): void {
296355
view.refresh();
297356
}
298357
}
299358

300359
export class FindAgentSessionAction extends ViewAction<AgentSessionsView> {
360+
301361
constructor() {
302362
super({
303363
id: 'agentSessionsView.find',
@@ -311,6 +371,7 @@ export class FindAgentSessionAction extends ViewAction<AgentSessionsView> {
311371
viewId: AGENT_SESSIONS_VIEW_ID
312372
});
313373
}
374+
314375
runInView(accessor: ServicesAccessor, view: AgentSessionsView): void {
315376
view.openFind();
316377
}

src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ import { Event } from '../../../../../base/common/event.js';
2323
import { Disposable } from '../../../../../base/common/lifecycle.js';
2424
import { ITreeContextMenuEvent } from '../../../../../base/browser/ui/tree/tree.js';
2525
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
26-
import { getFlatActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
26+
import { Separator } from '../../../../../base/common/actions.js';
2727
import { IChatService } from '../../common/chatService.js';
2828
import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js';
2929
import { TreeFindMode } from '../../../../../base/browser/ui/tree/abstractTree.js';
3030
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
3131
import { IMarshalledChatSessionContext } from '../actions/chatSessionActions.js';
32-
import { distinct } from '../../../../../base/common/arrays.js';
3332
import { IAgentSessionsService } from './agentSessionsService.js';
3433
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
3534
import { IListStyles } from '../../../../../base/browser/ui/list/listWidget.js';
@@ -184,6 +183,8 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
184183
providerType: session.providerType
185184
});
186185

186+
session.setRead(true); // mark as read when opened
187+
187188
let sessionOptions: IChatEditorOptions;
188189
if (isLocalAgentSessionItem(session)) {
189190
sessionOptions = {};
@@ -223,11 +224,13 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
223224
const provider = await this.chatSessionsService.activateChatSessionItemProvider(session.providerType);
224225
const contextOverlay = getSessionItemContextOverlay(session, provider, this.chatService, this.editorGroupsService);
225226
contextOverlay.push([ChatContextKeys.isCombinedAgentSessionsViewer.key, true]);
227+
contextOverlay.push([ChatContextKeys.isReadAgentSession.key, session.isRead()]);
228+
contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, session.isArchived()]);
226229
const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay));
227230

228231
const marshalledSession: IMarshalledChatSessionContext = { session, $mid: MarshalledId.ChatSessionContext };
229232
this.contextMenuService.showContextMenu({
230-
getActions: () => distinct(getFlatActionBarActions(menu.getActions({ arg: marshalledSession, shouldForwardArgs: true })), action => action.id),
233+
getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)),
231234
getAnchor: () => anchor,
232235
getActionsContext: () => marshalledSession,
233236
});

src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ export function getAgentChangesSummary(changes: IAgentSession['changes']) {
8686
export interface IAgentSession extends IAgentSessionData {
8787
isArchived(): boolean;
8888
setArchived(archived: boolean): void;
89+
90+
isRead(): boolean;
91+
setRead(read: boolean): void;
8992
}
9093

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

124+
interface IAgentSessionState {
125+
readonly archived: boolean;
126+
readonly read: number /* last date turned read */;
127+
}
128+
121129
//#endregion
122130

123131
export class AgentSessionsModel extends Disposable implements IAgentSessionsModel {
@@ -341,13 +349,15 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
341349
return {
342350
...data,
343351
isArchived: () => this.isArchived(data),
344-
setArchived: (archived: boolean) => this.setArchived(data, archived)
352+
setArchived: (archived: boolean) => this.setArchived(data, archived),
353+
isRead: () => this.isRead(data),
354+
setRead: (read: boolean) => this.setRead(data, read),
345355
};
346356
}
347357

348358
//#region States
349359

350-
private readonly sessionStates: ResourceMap<{ archived: boolean }>;
360+
private readonly sessionStates: ResourceMap<IAgentSessionState>;
351361

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

361-
this.sessionStates.set(session.resource, { archived });
371+
const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 };
372+
this.sessionStates.set(session.resource, { ...state, archived });
373+
374+
this._onDidChangeSessions.fire();
375+
}
376+
377+
private isRead(session: IInternalAgentSessionData): boolean {
378+
const readDate = this.sessionStates.get(session.resource)?.read;
379+
380+
return (readDate ?? 0) >= (session.timing.endTime ?? session.timing.startTime);
381+
}
382+
383+
private setRead(session: IInternalAgentSessionData, read: boolean): void {
384+
if (read === this.isRead(session)) {
385+
return; // no change
386+
}
387+
388+
const state = this.sessionStates.get(session.resource) ?? { archived: false, read: 0 };
389+
this.sessionStates.set(session.resource, { ...state, read: read ? Date.now() : 0 });
362390

363391
this._onDidChangeSessions.fire();
364392
}
@@ -397,9 +425,8 @@ interface ISerializedAgentSession {
397425
};
398426
}
399427

400-
interface ISerializedAgentSessionState {
428+
interface ISerializedAgentSessionState extends IAgentSessionState {
401429
readonly resource: UriComponents;
402-
readonly archived: boolean;
403430
}
404431

405432
class AgentSessionsCache {
@@ -482,17 +509,18 @@ class AgentSessionsCache {
482509

483510
//#region States
484511

485-
saveSessionStates(states: ResourceMap<{ archived: boolean }>): void {
512+
saveSessionStates(states: ResourceMap<IAgentSessionState>): void {
486513
const serialized: ISerializedAgentSessionState[] = Array.from(states.entries()).map(([resource, state]) => ({
487514
resource: resource.toJSON(),
488-
archived: state.archived
515+
archived: state.archived,
516+
read: state.read
489517
}));
490518

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

494-
loadSessionStates(): ResourceMap<{ archived: boolean }> {
495-
const states = new ResourceMap<{ archived: boolean }>();
522+
loadSessionStates(): ResourceMap<IAgentSessionState> {
523+
const states = new ResourceMap<IAgentSessionState>();
496524

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

505533
for (const entry of cached) {
506-
states.set(URI.revive(entry.resource), { archived: entry.archived });
534+
states.set(URI.revive(entry.resource), {
535+
archived: entry.archived,
536+
read: entry.read
537+
});
507538
}
508539
} catch {
509540
// invalid data in storage, fallback to empty states

src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
157157

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

162163
// Details Actions
@@ -201,7 +202,11 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
201202
return Codicon.error;
202203
}
203204

204-
return Codicon.pass;
205+
if (!session.isRead() && !session.isArchived()) {
206+
return Codicon.circleFilled;
207+
}
208+
209+
return Codicon.blank;
205210
}
206211

207212
private renderDescription(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {

src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@
6767
.agent-session-icon {
6868
flex-shrink: 0;
6969
font-size: 16px;
70+
71+
&.codicon.codicon-session-in-progress {
72+
color: var(--vscode-textLink-foreground);
73+
}
74+
75+
&.codicon.codicon-error {
76+
color: var(--vscode-errorForeground);
77+
}
78+
79+
&.codicon.codicon-circle-filled {
80+
color: var(--vscode-textLink-foreground);
81+
}
7082
}
7183
}
7284

src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class ChatSessionsViewContrib extends Disposable implements IWorkbenchCon
121121
const providersWithDisplayNames = otherProviders.map(provider => {
122122
const extContribution = extensionPointContributions.find(c => c.type === provider.chatSessionType);
123123
if (!extContribution) {
124-
this.logService.warn(`No extension contribution found for chat session type: ${provider.chatSessionType}`);
124+
this.logService.trace(`No extension contribution found for chat session type: ${provider.chatSessionType}`);
125125
return null;
126126
}
127127
return {

src/vs/workbench/contrib/chat/common/chatContextKeys.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@ export namespace ChatContextKeys {
9696
export const agentSessionsViewerOrientation = new RawContextKey<number>('agentSessionsViewerOrientation', undefined, { type: 'number', description: localize('agentSessionsViewerOrientation', "Orientation of the agent sessions view in the chat view.") });
9797
export const agentSessionsViewerPosition = new RawContextKey<number>('agentSessionsViewerPosition', undefined, { type: 'number', description: localize('agentSessionsViewerPosition', "Position of the agent sessions view in the chat view.") });
9898
export const agentSessionType = new RawContextKey<string>('chatSessionType', '', { type: 'string', description: localize('agentSessionType', "The type of the current agent session item.") });
99-
export const hasAgentSessionChanges = new RawContextKey<boolean>('chatSessionHasAgentChanges', false, { type: 'boolean', description: localize('chatSessionHasAgentChanges', "True when the current agent session item has changes.") });
100-
export const isArchivedAgentSession = new RawContextKey<boolean>('agentIsArchived', false, { type: 'boolean', description: localize('agentIsArchived', "True when the agent session item is archived.") });
101-
export const isActiveAgentSession = new RawContextKey<boolean>('agentIsActive', false, { type: 'boolean', description: localize('agentIsActive', "True when the agent session is currently active (not deletable).") });
99+
export const hasAgentSessionChanges = new RawContextKey<boolean>('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") });
100+
export const isArchivedAgentSession = new RawContextKey<boolean>('agentSessionIsArchived', false, { type: 'boolean', description: localize('agentSessionIsArchived', "True when the agent session item is archived.") });
101+
export const isReadAgentSession = new RawContextKey<boolean>('agentSessionIsRead', false, { type: 'boolean', description: localize('agentSessionIsRead', "True when the agent session item is read.") });
102+
export const isActiveAgentSession = new RawContextKey<boolean>('agentSessionIsActive', false, { type: 'boolean', description: localize('agentSessionIsActive', "True when the agent session is currently active (not deletable).") });
102103

103104
export const isKatexMathElement = new RawContextKey<boolean>('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") });
104105
}

0 commit comments

Comments
 (0)