Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
53c4801
sessions: add agent attempt comparisons
meganrogge Sep 10, 2026
f718691
sessions: address agent comparison feedback
meganrogge Sep 10, 2026
a9a2bbd
sessions: fix agent comparison CI
meganrogge Sep 10, 2026
3009205
sessions: register comparison service in fixture
meganrogge Sep 10, 2026
91f1f9d
sessions: refresh comparison fixture modules
meganrogge Sep 10, 2026
42b0027
sessions: invalidate comparison fixture cache
meganrogge Sep 10, 2026
b4c6a26
sessions: stabilize comparison fixtures
meganrogge Sep 10, 2026
5f6af65
sessions: update comparison screenshots
meganrogge Sep 10, 2026
a4c9a19
sessions: remove fixture cache diagnostics
meganrogge Sep 10, 2026
4bec7c0
sessions: add agent comparison attempt setup
meganrogge Sep 11, 2026
3aef817
sessions: refine agent attempt comparisons
meganrogge Sep 14, 2026
743c95a
sessions: add post-judge review flow
meganrogge Sep 14, 2026
2066ec7
sessions: tile comparison attempts while running
anthonykim1 Sep 14, 2026
d0ca7f4
sessions: refine comparison review completion
meganrogge Sep 14, 2026
2525c87
sessions: polish comparison progress and actions
meganrogge Sep 14, 2026
77f1c16
sessions: use tmux-style comparison grid layouts
anthonykim1 Sep 14, 2026
c401dde
sessions: move comparison results into judge
meganrogge Sep 14, 2026
e240d56
Merge remote-tracking branch 'meganrogge/agents/vscode-issue-335085-i…
meganrogge Sep 14, 2026
842a95b
sessions: prioritize comparison review sessions
meganrogge Sep 14, 2026
8fdef5c
sessions: fix comparison attempt scrolling
meganrogge Sep 14, 2026
bedd1d4
Merge origin/main into agents/vscode-issue-335085-implementation
meganrogge Sep 14, 2026
94a3e2a
sessions: improve comparison accessibility
meganrogge Sep 14, 2026
39bbfdd
sessions: improve comparison judge reliability
meganrogge Sep 14, 2026
8057ff0
sessions: use stable attempt numbers in judge verdicts
meganrogge Sep 14, 2026
69e388a
sessions: fix parallel agents action label
meganrogge Sep 14, 2026
05960bf
sessions: add customizable synthesis plans
anthonykim1 Sep 14, 2026
5c6bd3a
sessions: simplify synthesis option labels
anthonykim1 Sep 14, 2026
6b658a4
sessions: remove visible attempt number prefixes
anthonykim1 Sep 14, 2026
fc2bb65
sessions: hide editor when opening comparison grid
anthonykim1 Sep 15, 2026
5adc83a
sessions: refine comparison progress UI
meganrogge Sep 15, 2026
b25a00c
Merge remote comparison updates
meganrogge Sep 15, 2026
80626e4
sessions: fix comparison tool CI
meganrogge Sep 15, 2026
a59334c
sessions: harden comparison lifecycle
meganrogge Sep 15, 2026
25b364f
sessions: type comparison launch results
meganrogge Sep 15, 2026
25235f9
sessions: add close affordance to tiled headers
anthonykim1 Sep 15, 2026
3d310a3
sessions: open comparison setup before config resolves
meganrogge Sep 15, 2026
c091619
Merge branch 'agents/vscode-issue-335085-implementation' of github.co…
meganrogge Sep 15, 2026
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
4 changes: 4 additions & 0 deletions build/lib/i18n.resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,10 @@
"name": "vs/sessions/contrib/sessions",
"project": "vscode-sessions"
},
{
"name": "vs/sessions/contrib/sessionComparison",
"project": "vscode-sessions"
},
{
"name": "vs/sessions/contrib/sessionInputBanners",
"project": "vscode-sessions"
Expand Down
1 change: 1 addition & 0 deletions src/vs/sessions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Do not turn those files into general Sessions guidance.
| Session-aware layout capture and restoration | [LAYOUT_CONTROLLER.md](LAYOUT_CONTROLLER.md) |
| Single-pane behavior scenarios | [SINGLE_PANE_SCENARIOS.md](SINGLE_PANE_SCENARIOS.md) |
| Sessions sidebar list | [SESSIONS_LIST.md](SESSIONS_LIST.md) |
| Multi-harness implementation comparisons | [SESSION_COMPARISONS.md](SESSION_COMPARISONS.md) |
| Phone layout and mobile components | [MOBILE.md](MOBILE.md) |
| AI customizations | [AI_CUSTOMIZATIONS.md](AI_CUSTOMIZATIONS.md) |
| Copilot customizations | [copilot-customizations-spec.md](copilot-customizations-spec.md) |
Expand Down
44 changes: 44 additions & 0 deletions src/vs/sessions/SESSION_COMPARISONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Session comparison architecture

> **Specification change gate:** Update this document only when comparison ownership, participant roles, persistence, or lifecycle invariants change.

## Scope

Session comparisons run the same task through multiple Sessions providers and preserve every implementation in an isolated worktree. The workflow is provider-neutral: comparison code uses `ISessionsManagementService`, while providers remain responsible for resolving their own model identifiers, creating worktrees, and deleting sessions.

## Ownership

| Concern | Owner |
|---|---|
| Comparison records, participant lifecycle, selection, and cleanup | `ISessionComparisonService` |
| Harness and shared-model selection | new-session composer |
| Session creation, model resolution, and worktree isolation | Sessions provider through `ISessionsManagementService` |
| Attempt evidence and user actions | comparison editor |
| Structured recommendation | visible Judge child session and `completeAttemptComparison` tool |

Comparison records are persisted in profile storage. Session and chat resources remain provider-owned identities; the comparison record does not duplicate provider session state.

## Participant hierarchy

Each comparison has one visible coordinator session. Attempts, the Judge, and an optional synthesis are created with `createdBySession` pointing to that coordinator:

- **Coordinator:** stable parent and grouping anchor; it does not implement the task.
- **Attempt:** one selected harness, one provider-resolved instance of the shared logical model, and one isolated worktree.
- **Judge:** starts after at least two successfully launched attempts reach a terminal state and submits one structured verdict.
- **Synthesis:** optional new attempt using the recommended or selected attempt's harness. It never mutates an original attempt.

The ordinary session-group and child-session mechanisms render this hierarchy. Comparison code must not create a second session tree.

## Lifecycle invariants

1. The prompt, attachments, workspace, branch, permission level, and logical model selection are shared across attempts.
2. Every harness must support worktree configuration and resolve the selected logical model to its provider-local model identifier.
3. Attempts launch concurrently. One launch failure is recorded without deleting successful attempts.
4. The Judge receives references to successful attempts and must classify tests, build, lint, and diagnostics as `passed`, `failed`, `notRun`, or `unknown`.
5. Selecting an attempt opens its session and records a preference; it does not apply changes to the user's working tree.
6. Synthesis creates a new isolated child. Original attempts remain until an explicit, confirmed discard.
7. Cleanup reports partial deletion failures and retains records for attempts that could not be deleted.

## Evidence

The comparison editor derives file counts, diff size, elapsed time, file overlap, attempt-specific files, and starting-source/base-branch agreement from provider-neutral session state. Missing evidence is shown as unknown rather than inferred as successful or equal.
8 changes: 8 additions & 0 deletions src/vs/sessions/contrib/chat/browser/newChatInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
renderRepositoryControls?: boolean;
sessionTypePickerOptions?: ISessionTypePickerOptions;
supportsBackground?: boolean;
sendButtonLabel?: IObservable<string | undefined>;
deferredNotificationsEnabled?: IObservable<boolean>;
petHostPreferred?: IObservable<boolean>;
getChatPetPlatformElements?: () => readonly HTMLElement[];
Expand Down Expand Up @@ -1204,6 +1205,13 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
ariaLabel: localize('send', "Send"),
}));
sendButton.icon = Codicon.arrowUpCompact;
if (this.options.sendButtonLabel) {
this._register(autorun(reader => {
const label = this.options.sendButtonLabel?.read(reader);
sendButton.label = label ?? '';
sendButton.element.ariaLabel = label ?? localize('send', "Send");
}));
Comment thread
meganrogge marked this conversation as resolved.
}
// Hold Alt while clicking Send to start the session in the background.
this._register(sendButton.onDidClick(e => this._send(!!this.options.supportsBackground && !!(e as MouseEvent | KeyboardEvent | undefined)?.altKey)));
}
Expand Down
72 changes: 71 additions & 1 deletion src/vs/sessions/contrib/chat/browser/newChatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com
import { isCancellationError, onUnexpectedError } from '../../../../base/common/errors.js';
import { Event } from '../../../../base/common/event.js';
import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { constObservable, derived, derivedObservableWithCache, autorun, IObservable, observableFromEvent, observableSignalFromEvent } from '../../../../base/common/observable.js';
import { constObservable, derived, derivedObservableWithCache, autorun, IObservable, observableFromEvent, observableSignalFromEvent, observableValue } from '../../../../base/common/observable.js';
import { isWeb } from '../../../../base/common/platform.js';
import { basename } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
Expand All @@ -20,6 +20,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
import { localize } from '../../../../nls.js';
Expand Down Expand Up @@ -58,6 +59,8 @@ import { Menus } from '../../../browser/menus.js';
import { getAdditionalFolderContextId, getAdditionalRepositoryContextId } from '../common/newChatContextIds.js';
import { UNIFIED_WORKSPACE_PICKER_SETTING } from '../common/constants.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ISessionComparisonService, SessionComparisonParticipantRole } from '../../../services/sessions/common/sessionComparison.js';
import { OPEN_SESSION_COMPARISON_COMMAND_ID } from '../../sessionComparison/common/sessionComparison.js';

// #region --- New Chat Widget ---

Expand Down Expand Up @@ -99,6 +102,8 @@ export class NewChatWidget extends Disposable {

/** In-flight background sends awaiting confirmation before their comments are cleared. */
private readonly _pendingBackgroundSends = this._register(new DisposableMap<object>());
private readonly _comparisonMode = observableValue(this, false);
private readonly _comparisonPicks = observableValue<readonly IPickedSessionType[]>(this, []);

/**
* Tracks whether the workspace picker is currently rendered (vs replaced by
Expand Down Expand Up @@ -131,6 +136,8 @@ export class NewChatWidget extends Disposable {
@IStorageService private readonly storageService: IStorageService,
@INewSessionComposerService newSessionComposerService: INewSessionComposerService,
@ICommandService private readonly commandService: ICommandService,
@ISessionComparisonService private readonly sessionComparisonService: ISessionComparisonService,
@INotificationService private readonly notificationService: INotificationService,
) {
super();
this._workspacePickerVisibleKey = SessionWorkspacePickerVisibleContext.bindTo(contextKeyService);
Expand All @@ -144,6 +151,7 @@ export class NewChatWidget extends Disposable {
if (activeSession && activeSession.isCreated.read(reader)) {
return prev;
}

return activeSession;
});

Expand Down Expand Up @@ -202,6 +210,9 @@ export class NewChatWidget extends Disposable {
if (session.loading.read(reader)) {
return false;
}
if (this._comparisonMode.read(reader) && this._comparisonPicks.read(reader).length < 2) {
return false;
}
return true;
});

Expand All @@ -211,6 +222,9 @@ export class NewChatWidget extends Disposable {
});
const hasFeedback = derived(this, reader => this._feedbackItems.read(reader).length > 0);
const canSubmitWithoutSession = derived(this, reader => !this._session.read(reader));
const sendButtonLabel = derived(this, reader => this._comparisonMode.read(reader)
? localize('newSession.runAttempts', "Run {0} Attempts", this._comparisonPicks.read(reader).length)
: undefined);
const deferredNotificationsEnabled = observableFromEvent(
this,
this.storageService.onDidChangeValue(StorageScope.APPLICATION, TOTAL_SESSIONS_KEY, this._store),
Expand All @@ -232,12 +246,14 @@ export class NewChatWidget extends Disposable {
historyKey: constObservable(undefined), // no persisted history for the new-session view
placeholder: localize('newSessionPromptPlaceholder', "Pitch your idea"),
supportsBackground: true,
sendButtonLabel,
deferredNotificationsEnabled,
petHostPreferred: this.options.petHostPreferred,
getChatPetPlatformElements: () => this._workspacePicker.getChatPetPlatformElements(),
onDidChangeChatPetPlatform: this._workspacePicker.onDidChangeChatPetPlatform,
sessionTypePickerOptions: {
prepareSessionTypeSelection: pick => this._prepareSessionTypeSelection(pick),
showComparisonToggle: true,
},
});
this._register(toDisposable(() => newChatInput.saveState()));
Expand Down Expand Up @@ -332,6 +348,10 @@ export class NewChatWidget extends Disposable {
await this._onWorkspaceSelected(this._workspacePicker.selectedFolderUri, pick);
this._newChatInput.focus();
}));
this._register(this._newChatInput.sessionTypePicker.onDidChangeComparisonPicks(picks => {
this._comparisonMode.set(this._newChatInput.sessionTypePicker.isComparisonMode(), undefined);
this._comparisonPicks.set(picks, undefined);
}));
this._register(this.sessionsManagementService.onDidChangeSessionTypes(() => this._restoreNoWorkspaceDraft()));

this._register(this.configurationService.onDidChangeConfiguration(e => {
Expand Down Expand Up @@ -960,6 +980,56 @@ export class NewChatWidget extends Disposable {
}
}

if (this._comparisonMode?.get()) {
const workspace = this._workspacePicker.selectedFolderUri;
if (!workspace) {
this._workspacePicker.showPicker();
return false;
}
const availableTypes = this.sessionsManagementService.getSessionTypesForFolder(workspace);
const modelId = this._newChatInput.selectedModelState.get().currentModel?.identifier;
const harnesses = this._comparisonPicks.get().flatMap(pick => {
const type = availableTypes.find(candidate =>
candidate.providerId === pick.providerId && candidate.sessionType.id === pick.sessionTypeId);
const resolution = type && modelId
? this.sessionsProvidersService.getProvider(type.providerId)?.getModelsSnapshotForCreation?.(workspace, type.sessionType.id, modelId).desiredModelResolution
: undefined;
const resolvedModelId = resolution?.kind === 'available' ? resolution.model.identifier : undefined;
return type ? [{
providerId: pick.providerId,
sessionTypeId: pick.sessionTypeId,
label: type.sessionType.label,
modelId: resolvedModelId,
}] : [];
});
if (harnesses.length < 2) {
return false;
}
if (modelId && harnesses.some(harness => !harness.modelId)) {
this.notificationService.error(localize('sessionComparison.modelUnavailable', "The selected model is not available for every comparison harness. Choose a model shared by all selected harnesses."));
return false;
}
try {
this.sessionsService.unsetNewSession();
const comparison = await this.sessionComparisonService.startComparison({
workspace,
prompt: request,
attachedContext: requestContext.size > 0 ? [...requestContext.values()] : undefined,
harnesses,
Comment thread
meganrogge marked this conversation as resolved.
Outdated
});
const coordinator = comparison.participants.find(participant =>
participant.role === SessionComparisonParticipantRole.Coordinator)?.sessionResource;
if (coordinator) {
await this.sessionsService.openSession(coordinator, { source: 'chat' });
}
await this.commandService.executeCommand(OPEN_SESSION_COMPARISON_COMMAND_ID, comparison.id);
return true;
} catch (error) {
this.logService.error('Failed to start session comparison:', error);
return false;
}
Comment thread
meganrogge marked this conversation as resolved.
}

// Capture the composer's workspace selection before the send: a
// background send consumes the in-flight new session and resets the
// new-session view, so we re-seed a fresh pending session afterwards
Expand Down
Loading
Loading