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
3 changes: 2 additions & 1 deletion src/vs/sessions/SESSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Each session operates on an **`ISessionWorkspace`** containing one or more **`IS

Workspaces carry a `group` label (e.g., `"Local"`, `"Remote"`) used by the workspace picker to organize entries into tabs via the `SESSION_WORKSPACE_GROUP_LOCAL` / `SESSION_WORKSPACE_GROUP_REMOTE` constants.

Tasks with `runOptions.runOn === "worktreeCreated"` are dispatched client-side only for newly created sessions, after the session reports a concrete `gitRepository.workTreeUri`. Restored sessions and runtimes that declare `capabilities.runsWorktreeCreatedTasks` are skipped so setup tasks are not re-run on window open or double-run with server-side provisioning; untitled placeholders are deferred until they become committed worktree sessions.
Tasks with `runOptions.runOn === "worktreeCreated"` are dispatched client-side only for sessions that this window has just started. `SessionsManagementService` emits `onDidStartSession` from `sendNewChatRequest` after `provider.sendRequest(...)` commits, and `WorktreeCreatedTaskDispatcher` tracks only those sessions until they report a concrete `gitRepository.workTreeUri`. Restored/synced catalog sessions and runtimes that declare `capabilities.runsWorktreeCreatedTasks` are skipped so setup tasks are not re-run on window open or double-run with server-side provisioning.

### Session Types

Expand Down Expand Up @@ -143,6 +143,7 @@ Sessions produce file changes organized into **`ISessionChangeset`** groups —
→ Management service opens the chat widget with that chat's resource
→ Delegates to provider.sendRequest(sessionId, chatResource, options)
→ Provider sends request, returns committed session
→ Management service fires onDidStartSession(committedSession)
→ isNewChatSession context → false

Agent-host providers seed new-session config from the last values picked in the
Expand Down
2 changes: 1 addition & 1 deletion src/vs/sessions/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
properties: {
[AGENT_HOST_RUN_WORKTREE_CREATED_TASKS_SETTING]: {
type: 'boolean',
default: false,
default: true,
Comment thread
connor4312 marked this conversation as resolved.
scope: ConfigurationScope.APPLICATION,
description: localize('chat.agentHost.runWorktreeCreatedTasks', "Whether to automatically run tasks tagged with `\"runOptions\": { \"runOn\": \"worktreeCreated\" }` when a new agent host session worktree is created. Manual `Run Task` invocations are unaffected."),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js';
import { autorun } from '../../../../base/common/observable.js';
import { registerAutorunSelfDisposable } from '../../../../base/common/observable.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';
import { isAgentHostProviderId } from '../../../common/agentHostSessionsProvider.js';
import { ISession, SessionStatus } from '../../../services/sessions/common/session.js';
import { ISessionsChangeEvent, ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
import { ISessionsTasksService } from './sessionsTasksService.js';

const LOG_PREFIX = '[WorktreeCreatedTaskDispatcher]';

/**
* Setting that controls whether `runOptions.runOn === 'worktreeCreated'`
* tasks are auto-dispatched for agent host sessions when a new worktree is
* created. Defaults to `false`. Manual `Run Task` invocations are unaffected.
* created. Defaults to `true`. Manual `Run Task` invocations are unaffected.
*/
export const AGENT_HOST_RUN_WORKTREE_CREATED_TASKS_SETTING = 'chat.agentHost.runWorktreeCreatedTasks';

Expand All @@ -41,7 +41,6 @@ export class WorktreeCreatedTaskDispatcher extends Disposable implements IWorkbe
// Track per-session disposables (one per in-flight session subscription) so
// we tear them down when the session is removed.
private readonly _sessionDisposables = this._register(new DisposableMap<string>());
private readonly _dispatchedSessions = new Set<string>();

constructor(
@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,
Expand All @@ -51,55 +50,33 @@ export class WorktreeCreatedTaskDispatcher extends Disposable implements IWorkbe
) {
super();

for (const session of this._sessionsManagementService.getSessions()) {
this._trackSession(session);
}

this._register(this._sessionsManagementService.onDidChangeSessions(e => this._onDidChangeSessions(e)));
this._register(this._sessionsManagementService.onDidStartSession(session => this._trackSession(session)));
this._register(this._sessionsManagementService.onDidChangeSessions(e => this._onDidRemoveSessions(e.removed)));
}

private _onDidChangeSessions(e: ISessionsChangeEvent): void {
const removedTrackedSessions: ISession[] = [];
for (const session of e.removed) {
if (this._sessionDisposables.get(session.sessionId) && !this._dispatchedSessions.has(session.sessionId)) {
removedTrackedSessions.push(session);
}
private _onDidRemoveSessions(removed: readonly ISession[]): void {
for (const session of removed) {
this._sessionDisposables.deleteAndDispose(session.sessionId);
this._dispatchedSessions.delete(session.sessionId);
}
for (const session of e.added) {
this._trackSession(session);
}
const replacement = e.added.length === 0 && e.changed.length === 1 && removedTrackedSessions.length === 1
? removedTrackedSessions[0]
: undefined;
for (const session of e.changed) {
this._trackSession(session, replacement?.providerId === session.providerId && replacement.sessionType === session.sessionType);
}
}

private _trackSession(session: ISession, allowReadySession = false): void {
private _trackSession(session: ISession): void {
if (session.capabilities.runsWorktreeCreatedTasks) {
// The session's runtime already runs these tasks itself.
return;
}
if (this._sessionDisposables.get(session.sessionId)) {
return;
}
if (!allowReadySession && !this._isPendingWorktreeSession(session)) {
return;
}

const store = new DisposableStore();
this._sessionDisposables.set(session.sessionId, store);

// Wait for the session to finish loading and report an actual worktree,
// then dispatch any pending worktreeCreated tasks once. Set
// `dispatched` synchronously before the await so any re-firing of the
// autorun observes it and bails.
let dispatched = false;
store.add(autorun(reader => {
if (session.loading.read(reader) || dispatched) {
// then dispatch any pending worktreeCreated tasks once. When dispatched,
// dispose the per-session subscription store to tear down this autorun.
registerAutorunSelfDisposable(store, reader => {
if (session.loading.read(reader)) {
return;
}
if (session.status.read(reader) === SessionStatus.Untitled) {
Expand All @@ -108,18 +85,9 @@ export class WorktreeCreatedTaskDispatcher extends Disposable implements IWorkbe
if (!session.workspace.read(reader)?.folders.some(folder => !!folder.gitRepository?.workTreeUri)) {
return;
}
dispatched = true;
this._dispatchedSessions.add(session.sessionId);
void this._dispatchWorktreeCreatedTasks(session);
}));
}

private _isPendingWorktreeSession(session: ISession): boolean {
return session.status.get() === SessionStatus.Untitled || session.loading.get() || !this._hasWorktree(session);
}

private _hasWorktree(session: ISession): boolean {
return session.workspace.get()?.folders.some(folder => !!folder.gitRepository?.workTreeUri) ?? false;
this._sessionDisposables.deleteAndDispose(session.sessionId);
this._dispatchWorktreeCreatedTasks(session);
Comment thread
connor4312 marked this conversation as resolved.
});
}

private async _dispatchWorktreeCreatedTasks(session: ISession): Promise<void> {
Expand Down
Loading
Loading