worktreeIsolation.ts ×39

Frontier kind: Code frontier

unlabeled · c_80e08b943cec

674 tests · 24030 LOC · 97 files · introduces 0 tests · 424 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
46 ranges424 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1934 ranges24030 lines · 97 files · Browse complete extent
All tests (intent)
674 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

2 files ranked by introduced lines: 424 introduced LOC across 46 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/shared/worktreeIsolation.ts 362 introduced LOC · 39 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- worktreeIsolation.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import * as fs from 'fs/promises';
7 > import { SequencerByKey } from '../../../../base/common/async.js';
8 > import { appendEscapedMarkdownInlineCode } from '../../../../base/common/htmlContent.js';
9 > import { Disposable } from '../../../../base/common/lifecycle.js';
10 > import { Schemas } from '../../../../base/common/network.js';
11 > import { basename } from '../../../../base/common/path.js';
12 > import { URI } from '../../../../base/common/uri.js';
13 > import { generateUuid } from '../../../../base/common/uuid.js';
14 > import { localize } from '../../../../nls.js';
15 > import { ILogService } from '../../../log/common/log.js';
16 > import { IAgentSessionProjectInfo } from '../../common/agentService.js';
17 > import { getBranchCompletions, IAgentHostGitService, IDefaultBranch, META_DIFF_BASE_BRANCH } from '../../common/agentHostGitService.js';
18 > import { ISchemaProperty, schemaProperty } from '../../common/agentHostSchema.js';
19 > import { ISessionDataService } from '../../common/sessionDataService.js';
20 > import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
21 > import { AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, ResponsePart, ResponsePartKind, Turn } from '../../common/state/sessionState.js';
22 > import { AGENT_BRANCH_PREFIX, AgentBranchNameGenerator, IAgentBranchNameGenerator } from './agentBranchNameGenerator.js';
23 > import { ICopilotApiService } from './copilotApiService.js';
24 >
25 > /**
26 > * Per-session-database metadata keys under which the worktree an agent
27 > * created for an isolated session is recorded. The string values keep the
28 > * historical `copilot.worktree.*` prefix so sessions materialized by earlier
29 > * Copilot builds keep resolving their worktree on archive / unarchive /
30 > * restore after this logic was unified across agents. All agents (Copilot,
31 > * Codex, Claude) now write and read these same keys; the per-session database
32 > * is already scoped by session, so there is no cross-agent collision.
33 > */
34 > const WORKTREE_META_BRANCH = 'copilot.worktree.branchName';
35 > const WORKTREE_META_PATH = 'copilot.worktree.path';
36 > export const WORKTREE_META_REPOSITORY_ROOT = 'copilot.worktree.repositoryRoot';
37 >
38 > /** Thrown when a persisted session working directory is missing and cannot be repaired. */
39 > export class SessionWorkingDirectoryMissingError extends Error {
40 > constructor(readonly workingDirectory: URI, readonly reason?: string) {
41 super(reason
42 ? localize('sessionWorkingDirectoryMissingWithReason', "This session couldn't be loaded because its worktree is missing and could not be recreated: {0}", reason)
44 this.name = 'SessionWorkingDirectoryMissingError';
45 }
47 >
48 > /** Default upper bound on branch names returned for the branch picker. */
49 > const BRANCH_COMPLETION_LIMIT = 25;
50 >
51 > interface ICreatedWorktree {
52 > readonly repositoryRoot: URI;
53 > readonly worktree: URI;
54 > }
55 >
56 > /**
57 > * The `<repo>.worktrees` sibling directory where per-session isolated
58 > * worktrees are created, e.g. `/src/vscode` → `/src/vscode.worktrees`.
59 > */
60 > export function getWorktreesRoot(repositoryRoot: URI): URI {
61 return URI.joinPath(repositoryRoot, '..', `${basename(repositoryRoot.fsPath)}.worktrees`);
62 }
64 > /**
65 > * Derives the on-disk worktree directory name from a branch name: strips the
66 > * caller-supplied prefix (e.g. the user's `git.branchPrefix`) and the built-in
67 > * `agents/` prefix so the directory stays concise, then flattens any remaining
68 > * path separators.
69 > */
70 > export function getWorktreeName(branchName: string, branchPrefix: string = ''): string {
71 let name = branchName;
72 if (branchPrefix && name.startsWith(branchPrefix)) {
78 return name.replace(/\//g, '-');
79 }
81 > /**
82 > * Builds the localized "Created isolated worktree for branch X" markdown shown
83 > * at the top of the first response in worktree-isolated sessions. The branch
84 > * name is wrapped as inline code so the localized template doesn't have to
85 > * embed markdown punctuation. The trailing blank line keeps the announcement
86 > * visually separated when it gets merged into the same markdown part as the
87 > * model's reply.
88 > */
89 > export function buildWorktreeAnnouncementText(branchName: string): string {
90 return localize(
91 'agentHost.worktreeCreated',
94 ) + '\n\n';
95 }
97 > /**
98 > * Returns a copy of `turns` where `announcement` has been prepended to the
99 > * first top-level assistant turn's first markdown response part. Used on
100 > * session restore so the worktree announcement remains visible after the
101 > * session is reopened. If no assistant content exists yet, a fresh markdown
102 > * part is inserted at the top of the first turn.
103 > */
104 > export function prependAnnouncementToFirstTurn(turns: readonly Turn[], announcement: string): readonly Turn[] {
105 if (turns.length === 0) {
106 return turns;
122 return result;
123 }
125 > /** Parameters for {@link WorktreeIsolation.resolveIsolationConfig}. */
126 > export interface IResolveIsolationConfigRequest {
127 > readonly workingDirectory: URI | undefined;
128 > readonly config: Record<string, unknown> | undefined;
129 > }
130 >
131 > /**
132 > * The isolation + branch schema contribution for an agent's
133 > * `resolveSessionConfig`. Callers merge {@link isolationProperty} (and
134 > * {@link branchProperty} / {@link worktreeBranchPrefixProperty} when present)
135 > * into their own schema and merge the default values ({@link isolationValue} /
136 > * {@link branchDefault}) into the defaults bag they pass to `validateOrDefault`.
137 > */
138 > export interface IIsolationConfigContribution {
139 > readonly isolationProperty: ISchemaProperty<'folder' | 'worktree'>;
140 > readonly branchProperty: ISchemaProperty<string> | undefined;
141 > /**
142 > * Read-only carrier for the client's `git.branchPrefix`. Declared for both
143 > * isolations (like `branch`) so the value rides `_config.values` and
144 > * survives isolation toggles; the host only consumes it for worktree
145 > * isolation (see {@link WorktreeIsolation.resolveWorkingDirectory}).
146 > */
147 > readonly worktreeBranchPrefixProperty: ISchemaProperty<string> | undefined;
148 > /** Read-only carrier for the client's `git.worktreeIncludeFiles`. */
149 > readonly worktreeIncludeFilesProperty: ISchemaProperty<readonly string[]> | undefined;
150 > readonly isolationValue: 'folder' | 'worktree';
151 > readonly branchDefault: string | undefined;
152 > readonly branchValue: string | undefined;
153 > }
154 >
155 > /** Parameters for {@link WorktreeIsolation.resolveWorkingDirectory}. */
156 > export interface IResolveWorkingDirectoryRequest {
157 > readonly sessionUri: URI;
158 > readonly sessionId: string;
159 > readonly workingDirectory: URI | undefined;
160 > readonly config: Record<string, unknown> | undefined;
161 > readonly prompt?: string;
162 > readonly githubToken?: string;
163 > }
164 >
165 > /**
166 > * Shared, per-agent controller for git-worktree session isolation. Owns the
167 > * full machinery Copilot pioneered so Codex and Claude get identical behavior:
168 > *
169 > * - advertising the `isolation` (`folder` / `worktree`) and `branch` session
170 > * config properties from `resolveSessionConfig` ({@link resolveIsolationConfig});
171 > * - completing branch names for the branch picker ({@link branchCompletions});
172 > * - creating the worktree on materialization and persisting its metadata
173 > * ({@link resolveWorkingDirectory});
174 > * - surfacing the "Created isolated worktree" announcement live on the first
175 > * turn ({@link takePendingAnnouncement}) and on restore
176 > * ({@link applyRestoreAnnouncement});
177 > * - cleaning up / recreating the worktree on dispose, archive, and unarchive.
178 > *
179 > * A single host-owned instance serves every agent: the orchestrator
180 > * ({@link AgentService}) creates it and drives the lifecycle so individual
181 > * agents stay unaware of the folder-vs-worktree distinction. Session state
182 > * (`_createdWorktrees`, pending markers, pending announcements) is keyed by the
183 > * globally-unique sessionId, so sharing one instance across agents is safe.
184 > */
185 > export class WorktreeIsolation extends Disposable {
186 >
187 > /**
188 > * Worktrees created by this agent in the current process, keyed by
189 > * sessionId. Used to remove the worktree on dispose / error and to
190 > * enumerate live worktrees during shutdown.
191 > */
192 > private readonly _createdWorktrees = new Map<string, ICreatedWorktree>();
193 >
194 > /**
195 > * Per-session announcement (markdown) emitted as a synthetic streaming
196 > * markdown part the first time the session sends a message. Surfaces the
197 > * "Created isolated worktree for branch X" message live during the first
198 > * turn; the same announcement is re-injected on restore via
199 > * {@link applyRestoreAnnouncement}.
200 > */
201 > private readonly _pendingFirstTurnAnnouncements = new Map<string, string>();
202 >
203 > /**
204 > * SessionIds of freshly-created worktree-isolation sessions whose worktree
205 > * has not yet been created (creation is deferred to the first send so the
206 > * user's prompt can drive branch naming). While a session is in this set the
207 > * host reports its working directory as "pending" ({@link isWorkingDirectoryPending})
208 > * so agents defer prewarming / materializing until {@link resolveOnFirstSend}
209 > * runs. Never populated for restored sessions — their worktree already exists
210 > * on disk and their persisted working directory already points at it.
211 > */
212 > private readonly _pending = new Set<string>();
213 >
214 > /** Fixed log label; one host-owned instance serves every agent. */
215 > private readonly _logLabel = 'AgentHost';
216 >
217 > /**
218 > * Serializes the worktree lifecycle per session so a first-send creation
219 > * ({@link resolveOnFirstSend}) never interleaves with archive/unarchive
220 > * cleanup ({@link cleanupWorktreeOnArchive} / {@link recreateWorktreeOnUnarchive})
221 > * or dispose ({@link removeCreatedWorktree}) for the same session — the
222 > * guarantee each agent previously enforced with its own sequencer.
223 > */
224 > private readonly _sequencer = new SequencerByKey<string>();
225 > private readonly _worktreeCreationSequencer = new SequencerByKey<string>();
226 >
227 > /** Branch-name generator for worktree sessions; created from {@link ICopilotApiService} unless a test supplies an override. */
228 > private readonly _branchNameGenerator: IAgentBranchNameGenerator;
229 >
230 > constructor(
231 branchNameGenerator: IAgentBranchNameGenerator | undefined,
232 @IAgentHostGitService private readonly _gitService: IAgentHostGitService,
238 this._branchNameGenerator = branchNameGenerator ?? new AgentBranchNameGenerator(copilotApiService, this._logService);
239 }
241 > /** SessionIds with a worktree created by this agent in the current process. */
242 > get createdWorktreeSessionIds(): readonly string[] {
243 return [...this._createdWorktrees.keys()];
244 }
246 > /**
247 > * Marks a fresh worktree-isolation session as pending — its worktree is
248 > * deferred to the first send. Called by the host while a creating session's
249 > * resolved config selects `worktree` isolation.
250 > */
251 > notePending(sessionId: string): void {
252 this._pending.add(sessionId);
253 }
255 > /** Clears a pending marker when a session will not materialize a worktree. */
256 > clearPending(sessionId: string): void {
257 this._pending.delete(sessionId);
258 }
260 > /**
261 > * Whether a session's worktree is still pending creation. The host exposes
262 > * this through {@link IAgentConfigurationService.isWorkingDirectoryPending} so
263 > * agents defer materialization until the host has resolved the worktree.
264 > */
265 > isWorkingDirectoryPending(sessionId: string): boolean {
266 return this._pending.has(sessionId);
267 }
269 > /** The worktree created for a session in this process, if any. */
270 > getResolvedWorktree(sessionId: string): URI | undefined {
271 return this._createdWorktrees.get(sessionId)?.worktree;
272 }
274 > /**
275 > * First-send worktree resolution: creates the worktree (when the session
276 > * selected `worktree` isolation on a git repo) and clears the pending marker
277 > * regardless of outcome, so a failed creation falls back to folder isolation
278 > * instead of leaving the session permanently "pending". Delegates to
279 > * {@link resolveWorkingDirectory}, which is idempotent per session.
280 > */
281 > async resolveOnFirstSend(request: IResolveWorkingDirectoryRequest): Promise<URI | undefined> {
282 return this._sequencer.queue(request.sessionId, async () => {
283 try {
288 });
289 }
291 > /**
292 > * Builds the `isolation` / `branch` schema contribution for
293 > * `resolveSessionConfig`. When {@link IResolveIsolationConfigRequest.workingDirectory}
294 > * is not a git repository (or has no commits yet) isolation is forced to
295 > * `folder` and no branch property is offered.
296 > */
297 > async resolveIsolationConfig(request: IResolveIsolationConfigRequest): Promise<IIsolationConfigContribution> {
298 const gitInfo = request.workingDirectory ? await this._getGitInfo(request.workingDirectory) : undefined;
299
373 return { isolationProperty, branchProperty, worktreeBranchPrefixProperty, worktreeIncludeFilesProperty, isolationValue, branchDefault, branchValue };
374 }
376 > /**
377 > * Branch-name completions for the branch picker. Callers forward this from
378 > * their `sessionConfigCompletions` when the requested property is
379 > * {@link SessionConfigKey.Branch}.
380 > */
381 > async branchCompletions(workingDirectory: URI | undefined, query?: string): Promise<{ items: { value: string; label: string }[] }> {
382 if (!workingDirectory) {
383 return { items: [] };
388 return { items: branchCompletions.map(branch => ({ value: branch, label: branch })) };
389 }
391 > /**
392 > * Resolves the effective working directory for a session that is about to
393 > * be materialized. When the session config selects `worktree` isolation on
394 > * a git repository, creates a fresh branch + worktree, records it for
395 > * cleanup, queues the first-turn announcement, persists the worktree
396 > * metadata, and returns the worktree URI. Otherwise returns the requested
397 > * working directory unchanged.
398 > */
399 > async resolveWorkingDirectory(request: IResolveWorkingDirectoryRequest): Promise<URI | undefined> {
400 const { config, workingDirectory, sessionId, sessionUri, prompt, githubToken } = request;
401 if (config?.[SessionConfigKey.Isolation] !== 'worktree' || !workingDirectory || typeof config[SessionConfigKey.Branch] !== 'string') {
467 return worktree;
468 }
470 > /** Resolves a persisted working directory, repairing a removed worktree when possible. */
471 > async resolveWorkingDirectoryForResume(sessionUri: URI, sessionId: string, workingDirectory: URI): Promise<URI> {
472 return this._sequencer.queue(sessionId, () => this._resolveWorkingDirectoryForResume(sessionUri, sessionId, workingDirectory));
473 }
475 > private async _resolveWorkingDirectoryForResume(sessionUri: URI, sessionId: string, workingDirectory: URI): Promise<URI> {
476 if (workingDirectory.scheme !== Schemas.file) {
477 return workingDirectory;
514 throw new SessionWorkingDirectoryMissingError(workingDirectory, recreateFailureReason);
515 }
517 > /**
518 > * Takes (and clears) the pending "worktree created" announcement for a
519 > * session so callers can emit it live as the first response part on the
520 > * first turn. Returns `undefined` when the session has no pending
521 > * announcement.
522 > */
523 > takePendingAnnouncement(sessionId: string): string | undefined {
524 const announcement = this._pendingFirstTurnAnnouncements.get(sessionId);
525 if (announcement !== undefined) {
528 return announcement;
529 }
531 > /**
532 > * Re-injects the worktree announcement into a restored transcript by
533 > * prepending it to the first turn. No-op when the session was not worktree
534 > * isolated. Callers forward the turns returned from their history-read path.
535 > *
536 > * The live path ({@link takePendingAnnouncement}) handles the very first
537 > * turn while the session is fresh; this path takes over on subsequent loads
538 > * (where the synthetic announcement is not part of the agent transcript).
539 > */
540 > async applyRestoreAnnouncement(sessionUri: URI, turns: readonly Turn[]): Promise<readonly Turn[]> {
541 const worktreeMeta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
542 if (!worktreeMeta?.branchName) {
545 return prependAnnouncementToFirstTurn(turns, buildWorktreeAnnouncementText(worktreeMeta.branchName));
546 }
548 > /**
549 > * Removes the worktree created for a session in the current process (if
550 > * any). Used on session dispose and on materialization failure.
551 > */
552 > async removeCreatedWorktree(sessionId: string): Promise<void> {
553 return this._sequencer.queue(sessionId, () => this._removeCreatedWorktree(sessionId));
554 }
556 > private async _removeCreatedWorktree(sessionId: string): Promise<void> {
557 this.clearPending(sessionId);
558 const worktree = this._createdWorktrees.get(sessionId);
568 }
569 }
571 > /**
572 > * Removes every worktree created by this agent in the current process.
573 > * Called from the agent's `shutdown` so no isolated worktree is leaked when
574 > * the provider is torn down, matching Copilot's shutdown drain.
575 > */
576 > async removeAllCreatedWorktrees(): Promise<void> {
577 await Promise.all(this.createdWorktreeSessionIds.map(sessionId => this.removeCreatedWorktree(sessionId)));
578 }
580 > /**
581 > * On archive, removes the worktree directory when its branch is preserved
582 > * and the working tree is clean, so the worktree can be recreated on
583 > * unarchive without losing work. Skips the removal when the branch is
584 > * missing or the tree is dirty.
585 > */
586 > async cleanupWorktreeOnArchive(sessionUri: URI, sessionId: string): Promise<void> {
587 return this._sequencer.queue(sessionId, () => this._cleanupWorktreeOnArchive(sessionUri, sessionId));
588 }
590 > private async _cleanupWorktreeOnArchive(sessionUri: URI, sessionId: string): Promise<void> {
591 const meta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
592 if (!meta?.worktreePath || !meta.repositoryRoot) {
631 }
632 }
634 > /**
635 > * On unarchive, recreates a previously cleaned-up worktree against its
636 > * preserved branch. No-op when the directory still exists or the branch is
637 > * missing.
638 > */
639 > async recreateWorktreeOnUnarchive(sessionUri: URI, sessionId: string): Promise<void> {
640 return this._sequencer.queue(sessionId, () => this._recreateWorktreeOnUnarchive(sessionUri, sessionId));
641 }
643 > private async _recreateWorktreeOnUnarchive(sessionUri: URI, sessionId: string): Promise<void> {
644 const meta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
645 if (!meta?.worktreePath || !meta.repositoryRoot) {
657 await this._recreateWorktree(sessionId, { branchName, worktreePath, repositoryRoot });
658 }
660 > private async _recreateWorktree(sessionId: string, meta: { readonly branchName: string; readonly worktreePath: URI; readonly repositoryRoot: URI }): Promise<{ readonly ok: true } | { readonly ok: false; readonly reason: string }> {
661 const { branchName, worktreePath, repositoryRoot } = meta;
662 const branchPresent = await this._gitService.branchExists(repositoryRoot, branchName).catch(() => false);
678 }
679 }
681 > /** Reads the persisted worktree metadata for a session, if any. */
682 > async readWorktreeMetadata(sessionUri: URI): Promise<{ branchName: string; worktreePath?: URI; repositoryRoot?: URI } | undefined> {
683 return this._readWorktreeMetadata(sessionUri);
684 }
686 > /**
687 > * Resolves the repository "project" for a worktree-isolated session from its
688 > * persisted worktree metadata. Worktree sessions run out of a
689 > * `<repo>.worktrees/<name>` directory, but in the sessions UI they must group
690 > * under the *repository* (e.g. `vscode`) — not the worktree folder — exactly
691 > * like Copilot. Returns the repository root as the project so agents can merge
692 > * it into the `project` field of the `IAgentSessionMetadata` reported from
693 > * `listSessions` / `getSessionMetadata`; without it a list refresh clears the
694 > * transient project set by the materialize event and the workspace reverts to
695 > * the worktree directory name. Returns `undefined` for sessions that were never
696 > * worktree-isolated, leaving the caller's own folder-based project untouched.
697 > */
698 > async resolveWorktreeProject(sessionUri: URI): Promise<IAgentSessionProjectInfo | undefined> {
699 const meta = await this._readWorktreeMetadata(sessionUri).catch(() => undefined);
700 return meta?.repositoryRoot ? projectFromRepositoryRoot(meta.repositoryRoot) : undefined;
701 }
703 > /**
704 > * Synchronous companion to {@link resolveWorktreeProject} for the
705 > * materialize-event path: the repository project for a worktree this agent
706 > * created in the current process, or `undefined` when the session has none.
707 > * Lets an agent supply the materialize event's `project` without an async
708 > * metadata read so a fresh worktree groups under the repository the moment it
709 > * materializes.
710 > */
711 > createdWorktreeProject(sessionId: string): IAgentSessionProjectInfo | undefined {
712 const worktree = this._createdWorktrees.get(sessionId);
713 return worktree ? projectFromRepositoryRoot(worktree.repositoryRoot) : undefined;
714 }
716 > private async _getGitInfo(workingDirectory: URI): Promise<{ currentBranch: string; defaultBranch: IDefaultBranch } | undefined> {
717 const repositoryRoot = await this._gitService.getRepositoryRoot(workingDirectory);
718 if (!repositoryRoot) {
730 return { currentBranch, defaultBranch };
731 }
733 > private async _resolveBranchStartPoint(repositoryRoot: URI, selectedBranch: string): Promise<string> {
734 const defaultBranch = await this._gitService.getDefaultBranch(repositoryRoot);
735 return defaultBranch?.name === selectedBranch
737 : selectedBranch;
738 }
740 > private async _writeWorktreeMetadata(sessionUri: URI, metadata: { branchName: string; baseBranch: string | undefined; worktreePath: URI; repositoryRoot: URI }): Promise<void> {
741 const dbRef = this._sessionDataService.openDatabase(sessionUri);
742 try {
754 }
755 }
757 > private async _readWorktreeMetadata(sessionUri: URI): Promise<{ branchName: string; worktreePath?: URI; repositoryRoot?: URI } | undefined> {
758 const ref = await this._sessionDataService.tryOpenDatabase(sessionUri);
759 if (!ref) {
776 }
777 }
779 > private async _isSessionArchived(sessionUri: URI): Promise<boolean> {
780 const ref = await this._sessionDataService.tryOpenDatabase(sessionUri);
781 if (!ref) {
792 }
793 }
795 >
796 > /**
797 > * Derives the repository {@link IAgentSessionProjectInfo} from a repository
798 > * root URI. The display name is the repo directory's basename (falling back to
799 > * the URI string for pathological roots), matching how Copilot names the
800 > * project via `resolveGitProject`.
801 > */
802 function projectFromRepositoryRoot(repositoryRoot: URI): IAgentSessionProjectInfo {
803 return { uri: repositoryRoot, displayName: basename(repositoryRoot.fsPath) || repositoryRoot.toString() };
804 }
806 > /**
807 > * Builds the repository {@link IAgentSessionProjectInfo} from a persisted
808 > * {@link WORKTREE_META_REPOSITORY_ROOT} value (a URI string), or `undefined`
809 > * when absent. Lets the host merge the repository project into a session's
810 > * catalog entry directly from a metadata batch it already read, without a
811 > * second database open.
812 > */
813 > export function worktreeProjectFromRepositoryRoot(repositoryRootRaw: string | undefined): IAgentSessionProjectInfo | undefined {
814 return repositoryRootRaw ? projectFromRepositoryRoot(URI.parse(repositoryRootRaw)) : undefined;
815 }
817 function errorMessage(error: unknown): string {
818 return error instanceof Error ? error.message : String(error);
819 }
821 async function fileExists(path: string): Promise<boolean> {
822 try {
src/vs/platform/agentHost/node/shared/agentBranchNameGenerator.ts 62 introduced LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentBranchNameGenerator.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { ILogService } from '../../../log/common/log.js';
7 > import { ICopilotApiService, type ICopilotUtilityChatMessage } from './copilotApiService.js';
8 >
9 > /**
10 > * Branch-name prefix for worktree-isolated agent sessions, e.g.
11 > * `agents/add-feature`. Shared by every agent-host provider (Copilot, Codex,
12 > * Claude) via {@link WorktreeIsolation}.
13 > */
14 > export const AGENT_BRANCH_PREFIX = 'agents/';
15 > const AGENT_BRANCH_SESSION_ID_SUFFIX_LENGTH = 8;
16 > const MAX_BRANCH_NAME_HINT_LENGTH = 48;
17 > const MIN_GENERATED_BRANCH_NAME_LENGTH = 8;
18 > const MAX_BRANCH_NAME_CANDIDATES = 100;
19 >
20 > export interface IAgentBranchNameGeneratorRequest {
21 > readonly sessionId: string;
22 > readonly message?: string;
23 > readonly githubToken?: string;
24 > readonly signal?: AbortSignal;
25 > /**
26 > * Optional prefix prepended before the built-in {@link AGENT_BRANCH_PREFIX}
27 > * when constructing the branch name (e.g. the user's `git.branchPrefix`
28 > * setting). An empty or omitted value preserves the historical
29 > * `agents/<hint>` naming.
30 > */
31 > readonly branchPrefix?: string;
32 > /**
33 > * Optional predicate used to check whether a candidate branch name collides
34 > * with an existing branch or its corresponding worktree path.
35 > */
36 > readonly branchNameCollides?: (branchName: string) => Promise<boolean>;
37 > }
38 >
39 > export interface IAgentBranchNameGenerator {
40 > generateBranchName(request: IAgentBranchNameGeneratorRequest): Promise<string>;
41 > }
42 >
43 > export class AgentBranchNameGenerator implements IAgentBranchNameGenerator {
44 >
45 > constructor(
46 @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
47 @ILogService private readonly _logService: ILogService,
48 ) { }
50 > async generateBranchName(request: IAgentBranchNameGeneratorRequest): Promise<string> {
51 const branchNameHint = (await this._generateBranchNameHint(request)) ?? getAgentBranchNameHintFromMessage(request.message ?? '');
52 return this._buildBranchName(request, branchNameHint);
53 }
55 > private async _generateBranchNameHint(request: IAgentBranchNameGeneratorRequest): Promise<string | undefined> {
56 const message = request.message?.trim();
57 if (!message || !request.githubToken) {
90 }
91 }
93 > private _buildBranchNamePrompt(userRequest: string): ICopilotUtilityChatMessage[] {
94 return [
95 {
111 ];
112 }
114 > private async _buildBranchName(request: IAgentBranchNameGeneratorRequest, branchNameHint: string | undefined): Promise<string> {
115 // Prepend the caller-supplied prefix (e.g. `git.branchPrefix`) ahead of
116 // the built-in `agents/` prefix. An empty/omitted value keeps the
135 throw new Error(`Unable to find an available branch name after checking ${MAX_BRANCH_NAME_CANDIDATES} candidates`);
136 }
138 >
139 > export function normalizeAgentBranchName(branchName: string): string {
140 // Only support alphanumeric characters and dashes for simplicity.
141 let normalized = branchName.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase();
151 return normalized;
152 }
154 > /**
155 > * Derive a slug-style branch-name hint from the user's first message. Used as
156 > * a local fallback when the utility branch name generation is unavailable.
157 > */
158 > export function getAgentBranchNameHintFromMessage(message: string): string | undefined {
159 const words = message
160 .toLowerCase()