mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
Support session pinning in VS Code workbench (#302853)
Support session pinning in VS Code workbench: enable pin/unpin everywhere, always show pinned above cap Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com>
This commit is contained in:
@@ -552,7 +552,6 @@ export class PinAgentSessionAction extends BaseAgentSessionAction {
|
||||
group: 'navigation',
|
||||
order: 0,
|
||||
when: ContextKeyExpr.and(
|
||||
IsSessionsWindowContext,
|
||||
ChatContextKeys.isPinnedAgentSession.negate(),
|
||||
ChatContextKeys.isArchivedAgentSession.negate()
|
||||
),
|
||||
@@ -561,7 +560,6 @@ export class PinAgentSessionAction extends BaseAgentSessionAction {
|
||||
group: '0_pin',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(
|
||||
IsSessionsWindowContext,
|
||||
ChatContextKeys.isPinnedAgentSession.negate(),
|
||||
ChatContextKeys.isArchivedAgentSession.negate()
|
||||
),
|
||||
@@ -588,7 +586,6 @@ export class UnpinAgentSessionAction extends BaseAgentSessionAction {
|
||||
group: 'navigation',
|
||||
order: 0,
|
||||
when: ContextKeyExpr.and(
|
||||
IsSessionsWindowContext,
|
||||
ChatContextKeys.isPinnedAgentSession,
|
||||
ChatContextKeys.isArchivedAgentSession.negate()
|
||||
),
|
||||
@@ -597,7 +594,6 @@ export class UnpinAgentSessionAction extends BaseAgentSessionAction {
|
||||
group: '0_pin',
|
||||
order: 1,
|
||||
when: ContextKeyExpr.and(
|
||||
IsSessionsWindowContext,
|
||||
ChatContextKeys.isPinnedAgentSession,
|
||||
ChatContextKeys.isArchivedAgentSession.negate()
|
||||
),
|
||||
|
||||
@@ -852,14 +852,22 @@ export class AgentSessionsDataSource extends Disposable implements IAsyncDataSou
|
||||
|
||||
const firstArchivedIndex = sortedSessions.findIndex(session => session.isArchived());
|
||||
const nonArchivedCount = firstArchivedIndex === -1 ? sortedSessions.length : firstArchivedIndex;
|
||||
const nonArchivedSessions = sortedSessions.slice(0, nonArchivedCount);
|
||||
const archivedSessions = sortedSessions.slice(nonArchivedCount);
|
||||
|
||||
const topSessions = sortedSessions.slice(0, Math.min(AgentSessionsDataSource.CAPPED_SESSIONS_LIMIT, nonArchivedCount));
|
||||
const othersSessions = sortedSessions.slice(topSessions.length);
|
||||
// All pinned sessions are always visible
|
||||
const pinnedSessions = nonArchivedSessions.filter(session => session.isPinned());
|
||||
const unpinnedSessions = nonArchivedSessions.filter(session => !session.isPinned());
|
||||
|
||||
// Add top sessions directly (no section header)
|
||||
result.push(...topSessions);
|
||||
// Take up to N non-pinned sessions from the sorted order (preserves NeedsInput prioritization)
|
||||
const topUnpinned = unpinnedSessions.slice(0, AgentSessionsDataSource.CAPPED_SESSIONS_LIMIT);
|
||||
const remainingUnpinned = unpinnedSessions.slice(AgentSessionsDataSource.CAPPED_SESSIONS_LIMIT);
|
||||
|
||||
// Add "More" section for the rest
|
||||
// Add pinned first, then top N non-pinned
|
||||
result.push(...pinnedSessions, ...topUnpinned);
|
||||
|
||||
// Add "More" section for the rest (remaining unpinned + archived)
|
||||
const othersSessions = [...remainingUnpinned, ...archivedSessions];
|
||||
if (othersSessions.length > 0) {
|
||||
result.push({
|
||||
section: AgentSessionSection.More,
|
||||
|
||||
@@ -536,16 +536,18 @@ suite('AgentSessionsDataSource', () => {
|
||||
assert.strictEqual(archivedSection.sessions[0].label, 'Session archived-pinned');
|
||||
});
|
||||
|
||||
test('pinned sessions are not capped into More section with capped grouping', () => {
|
||||
test('pinned sessions are always shown above the cap with capped grouping', () => {
|
||||
const now = Date.now();
|
||||
const sessions = [
|
||||
// Two pinned sessions — sorted to top by time so they appear in the flat portion
|
||||
createMockSession({ id: 'pinned1', isPinned: true, startTime: now }),
|
||||
createMockSession({ id: 'pinned2', isPinned: true, startTime: now - ONE_DAY }),
|
||||
// Additional unpinned sessions to exceed the cap and populate the More section
|
||||
// Recent unpinned sessions fill the top 3 by time
|
||||
createMockSession({ id: 's1', startTime: now }),
|
||||
createMockSession({ id: 's2', startTime: now - ONE_DAY }),
|
||||
createMockSession({ id: 's3', startTime: now - 2 * ONE_DAY }),
|
||||
// Unpinned overflow
|
||||
createMockSession({ id: 's4', startTime: now - 3 * ONE_DAY }),
|
||||
// Two pinned sessions with old timestamps — would fall outside top 3 by time alone
|
||||
createMockSession({ id: 'pinned1', isPinned: true, startTime: now - 4 * ONE_DAY }),
|
||||
createMockSession({ id: 'pinned2', isPinned: true, startTime: now - 5 * ONE_DAY }),
|
||||
];
|
||||
|
||||
const filter = createMockFilter({
|
||||
@@ -558,20 +560,97 @@ suite('AgentSessionsDataSource', () => {
|
||||
const mockModel = createMockModel(sessions);
|
||||
const result = Array.from(dataSource.getChildren(mockModel));
|
||||
const sections = getSectionsFromResult(result);
|
||||
const topSessions = result.filter((r): r is IAgentSession => !isAgentSessionSection(r));
|
||||
|
||||
// Capped grouping does not create a Pinned section — all sessions are
|
||||
// sorted by time and the top N appear as flat items, the rest in More.
|
||||
assert.strictEqual(sections.filter(s => s.section === AgentSessionSection.Pinned).length, 0);
|
||||
// Pinned sessions first, then up to 3 non-pinned sessions
|
||||
assert.deepStrictEqual(topSessions.map(s => s.label), [
|
||||
'Session pinned1',
|
||||
'Session pinned2',
|
||||
'Session s1',
|
||||
'Session s2',
|
||||
'Session s3',
|
||||
]);
|
||||
|
||||
// Only unpinned overflow goes to More
|
||||
const moreSection = sections.find(s => s.section === AgentSessionSection.More);
|
||||
assert.ok(moreSection);
|
||||
// Pinned sessions have recent timestamps so they land in the flat top portion,
|
||||
// not in the More section
|
||||
const moreLabels = moreSection.sessions.map(s => s.label);
|
||||
for (const label of moreLabels) {
|
||||
assert.notStrictEqual(label, 'Session pinned1');
|
||||
assert.notStrictEqual(label, 'Session pinned2');
|
||||
}
|
||||
assert.deepStrictEqual(moreSection.sessions.map(s => s.label), [
|
||||
'Session s4',
|
||||
]);
|
||||
});
|
||||
|
||||
test('more pinned sessions than cap limit are all shown', () => {
|
||||
const now = Date.now();
|
||||
const sessions = [
|
||||
createMockSession({ id: 'pinned1', isPinned: true, startTime: now }),
|
||||
createMockSession({ id: 'pinned2', isPinned: true, startTime: now - ONE_DAY }),
|
||||
createMockSession({ id: 'pinned3', isPinned: true, startTime: now - 2 * ONE_DAY }),
|
||||
createMockSession({ id: 'pinned4', isPinned: true, startTime: now - 3 * ONE_DAY }),
|
||||
// Unpinned session — still fits within the cap of 3 non-pinned
|
||||
createMockSession({ id: 'unpinned1', startTime: now - 4 * ONE_DAY }),
|
||||
];
|
||||
|
||||
const filter = createMockFilter({
|
||||
groupBy: AgentSessionsGrouping.Capped,
|
||||
excludeRead: false
|
||||
});
|
||||
const sorter = createMockSorter();
|
||||
const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter));
|
||||
|
||||
const mockModel = createMockModel(sessions);
|
||||
const result = Array.from(dataSource.getChildren(mockModel));
|
||||
const sections = getSectionsFromResult(result);
|
||||
const topSessions = result.filter((r): r is IAgentSession => !isAgentSessionSection(r));
|
||||
|
||||
// All 4 pinned + 1 unpinned (fits within cap of 3 non-pinned)
|
||||
assert.deepStrictEqual(topSessions.map(s => s.label), [
|
||||
'Session pinned1',
|
||||
'Session pinned2',
|
||||
'Session pinned3',
|
||||
'Session pinned4',
|
||||
'Session unpinned1',
|
||||
]);
|
||||
|
||||
// No More section needed since unpinned count (1) is within cap (3)
|
||||
const moreSection = sections.find(s => s.section === AgentSessionSection.More);
|
||||
assert.strictEqual(moreSection, undefined);
|
||||
});
|
||||
|
||||
test('unpinned NeedsInput session appears in the non-pinned section below pinned', () => {
|
||||
const now = Date.now();
|
||||
const sessions = [
|
||||
createMockSession({ id: 'needs-input', status: ChatSessionStatus.NeedsInput, startTime: now }),
|
||||
createMockSession({ id: 'pinned1', isPinned: true, startTime: now }),
|
||||
createMockSession({ id: 'pinned2', isPinned: true, startTime: now - ONE_DAY }),
|
||||
createMockSession({ id: 'pinned3', isPinned: true, startTime: now - 2 * ONE_DAY }),
|
||||
createMockSession({ id: 's1', startTime: now }),
|
||||
];
|
||||
|
||||
const filter = createMockFilter({
|
||||
groupBy: AgentSessionsGrouping.Capped,
|
||||
excludeRead: false
|
||||
});
|
||||
// Use real sorter to exercise NeedsInput prioritization in capped mode
|
||||
const sorter = new AgentSessionsSorter();
|
||||
const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter));
|
||||
|
||||
const mockModel = createMockModel(sessions);
|
||||
const result = Array.from(dataSource.getChildren(mockModel));
|
||||
const sections = getSectionsFromResult(result);
|
||||
const topSessions = result.filter((r): r is IAgentSession => !isAgentSessionSection(r));
|
||||
|
||||
// Pinned sessions come first, then up to 3 non-pinned (NeedsInput + s1 both fit in cap)
|
||||
assert.deepStrictEqual(topSessions.map(s => s.label), [
|
||||
'Session pinned1',
|
||||
'Session pinned2',
|
||||
'Session pinned3',
|
||||
'Session needs-input',
|
||||
'Session s1',
|
||||
]);
|
||||
|
||||
// All non-pinned fit within cap of 3, so no More section
|
||||
const moreSection = sections.find(s => s.section === AgentSessionSection.More);
|
||||
assert.strictEqual(moreSection, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user