src/vs/platform/agentHost/common/changesetUri.ts

370 LOC · 360 covered · 10 uncovered · 73 ranges · 3166 concepts · 44 introducers · 1406 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

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 related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- changesetUri.ts ×16
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 { localize } from '../../../nls.js';
7 > import { readSessionGitState, readSessionWorkspaceless, SessionLifecycle, type Changeset, type ISessionGitState, type ISessionWithDefaultChat, type URI } from './state/sessionState.js';
8 >
9 > /**
10 > * Helpers for building / parsing the URI clients subscribe to in order to
11 > * receive a {@link import('./state/protocol/state.js').ChangesetState}.
12 > *
13 > * Shapes recognised by this module:
14 > *
15 > * <sessionUri>/changeset/uncommitted
16 > * <sessionUri>/changeset/session
17 > * <sessionUri>/changeset/turn/<turnId>
18 > * <sessionUri>/changeset/compare/<originalTurnId>/<modifiedTurnId>
19 > *
20 > * Catalogue entries on `summary.changesets` may also advertise the
21 > * URI-template forms `<sessionUri>/changeset/turn/{turnId}` and
22 > * `<sessionUri>/changeset/compare/{originalTurnId}/{modifiedTurnId}`;
23 > * clients expand the template before subscribing.
24 > *
25 > * Keeping changeset URIs nested under the session URI namespace lets the
26 > * server cleanly tear down every changeset for a session when that session
27 > * is disposed (the reverse-lookup is just a string-prefix scan).
28 > */
29 >
30 > /** /** Stable id of the catalogue entry for the branch changeset. */
31 > const BRANCH_CHANGESET_ID = 'branch';
32 >
33 > /** Stable id of the catalogue entry for the uncommitted-changes changeset. */
34 > const UNCOMMITTED_CHANGESET_ID = 'uncommitted';
35 >
36 > /** Stable id of the catalogue entry for the session-wide changeset. */
37 > const SESSION_CHANGESET_ID = 'session';
38 >
39 > /** Path prefix used by per-turn changeset URIs (`turn/<turnId>`). */
40 > const TURN_CHANGESET_PREFIX = 'turn/';
41 >
42 > /** Template variable name used inside the per-turn URI template. */
43 > const TURN_TEMPLATE_VARIABLE = '{turnId}';
44 >
45 > /** Path prefix used by compare-turns changeset URIs (`compare/<originalTurnId>/<modifiedTurnId>`). */
46 > const COMPARE_CHANGESET_PREFIX = 'compare/';
47 >
48 > /** Template variable name for the original turn in the compare-turns URI template. */
49 > const COMPARE_ORIGINAL_TEMPLATE_VARIABLE = '{originalTurnId}';
50 >
51 > /** Template variable name for the modified turn in the compare-turns URI template. */
52 > const COMPARE_MODIFIED_TEMPLATE_VARIABLE = '{modifiedTurnId}';
53 >
54 > /** Localized human-readable label for the branch changeset entry. */
55 > export const branchChangesetLabel = (): string => localize('branchChangeset.label', "Branch Changes");
56 >
57 > /** Localized human-readable label for the session-wide changeset entry. */
58 > export const sessionChangesetLabel = (): string => localize('sessionChangeset.label', "All Changes");
59 >
60 > /** Localized human-readable description for the session-wide changeset entry. */
61 > export const sessionChangesetDescription = (): string => localize('sessionChangeset.description', "Show all changes made in this session");
62 >
63 > /** Localized human-readable label for the uncommitted-changes changeset entry. */
64 > export const uncommittedChangesetLabel = (): string => localize('uncommittedChangeset.label', "Uncommitted Changes");
65 >
66 > /** Localized human-readable description for the uncommitted-changes changeset entry. */
67 > export const uncommittedChangesetDescription = (): string => localize('uncommittedChangeset.description', "Show uncommitted changes in this session");
68 >
69 > /** Localized human-readable label for the per-turn changeset template entry. */
70 > export const thisTurnChangesetLabel = (): string => localize('thisTurnChangeset.label', "This Turn");
71 >
72 > /** Localized human-readable description for the per-turn changeset template entry. */
73 > export const thisTurnChangesetDescription = (): string => localize('thisTurnChangeset.description', "Show changes made in this turn");
74 >
75 > /** Localized human-readable label for the compare-turns changeset template entry. */
76 > export const compareTurnsChangesetLabel = (): string => localize('compareTurnsChangeset.label', "Compare Turns");
77 >
78 > /** Localized human-readable description for the compare-turns changeset template entry. */
79 > export const compareTurnsChangesetDescription = (): string => localize('compareTurnsChangeset.description', "Show changes made between different turns");
80 >
81 > /**
82 > * Returns the description shown next to the `Branch Changes` catalogue
83 > * entry. Prefers `${branchName} → ${baseBranchName}` when both values
84 > * are known (typical worktree-isolation case). If `baseBranchName` is
85 > * unknown, falls back to `${branchName} → ${upstreamBranchName}` when an
86 > * upstream is available. Finally falls back to `branchName` alone.
87 > * Returns `undefined` only when no branch name is known at all, so
88 > * callers can omit the description entirely.
89 > */
90 > export function formatBranchChangesetDescription(gitState: ISessionGitState): string | undefined {
91 > const { baseBranchName, branchName, upstreamBranchName } = gitState; changesetUri.ts ×3
92 >
93 > // Use branch name
94 > if (baseBranchName && branchName) {
95 > return `${branchName}${baseBranchName}`;
96 > }
97
98 // Use upstream branch name
99 > if (upstreamBranchName && branchName) { changesetUri.ts ×3
100 return `${branchName}${upstreamBranchName}`;
101 }
102
103 return branchName;
104 }
106 > /** Marker injected into a changeset URI's path. */
107 > const CHANGESET_PATH_SEGMENT = '/changeset/';
108 >
109 > /** Discriminates the well-known changeset URI shapes. */
110 > export const enum ChangesetKind {
111 > Branch = 'branch',
112 > Uncommitted = 'uncommitted',
113 > Session = 'session',
114 > Turn = 'turn',
115 > Compare = 'compare-turns',
116 > /** Producer-defined id we don't recognise (single-segment only). */
117 > Unknown = 'unknown',
118 > }
119 >
120 > export function buildBranchChangesetUri(sessionUri: URI): URI {
121 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${BRANCH_CHANGESET_ID}`; changesetUri.ts ×1
122 > }
124 > /** Returns the subscribable URI for the session-wide changeset. */
125 > export function buildSessionChangesetUri(sessionUri: URI): URI {
126 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${SESSION_CHANGESET_ID}`; changesetUri.ts ×1
127 > }
129 > /** Returns the subscribable URI for the uncommitted-changes changeset. */
130 > export function buildUncommittedChangesetUri(sessionUri: URI): URI {
131 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${UNCOMMITTED_CHANGESET_ID}`; changesetUri.ts ×1
132 > }
134 > /**
135 > * Returns the URI _template_ that catalogue entries advertise for the
136 > * per-turn changeset; clients expand `{turnId}` to build the
137 > * subscribable URI via {@link buildTurnChangesetUri}.
138 > */
139 > export function buildTurnChangesetUriTemplate(sessionUri: URI): URI {
140 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${TURN_CHANGESET_PREFIX}${TURN_TEMPLATE_VARIABLE}`; changesetUri.ts ×1
141 > }
143 > /** Returns the subscribable URI for the per-turn changeset of `turnId`. */
144 > export function buildTurnChangesetUri(sessionUri: URI, turnId: string): URI {
145 > if (!turnId || turnId.includes('/')) { changesetUri.ts ×1
146 > throw new Error(`buildTurnChangesetUri: turnId must be non-empty and not contain '/' (got ${JSON.stringify(turnId)})`); changesetUri.ts ×5
147 > }
148 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${TURN_CHANGESET_PREFIX}${turnId}`; changesetUri.ts ×1
149 > }
151 > /**
152 > * Returns the URI _template_ that catalogue entries advertise for the
153 > * compare-turns changeset; clients expand both `{originalTurnId}` and
154 > * `{modifiedTurnId}` to build the subscribable URI via
155 > * {@link buildCompareTurnsChangesetUri}.
156 > */
157 > export function buildCompareTurnsChangesetUriTemplate(sessionUri: URI): URI {
158 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${COMPARE_CHANGESET_PREFIX}${COMPARE_ORIGINAL_TEMPLATE_VARIABLE}/${COMPARE_MODIFIED_TEMPLATE_VARIABLE}`; changesetUri.ts ×1
159 > }
161 > /**
162 > * Returns the subscribable URI for the compare-turns changeset between
163 > * `originalTurnId` (the "from" endpoint) and `modifiedTurnId` (the "to"
164 > * endpoint). Diff direction is `originalTurnId → modifiedTurnId`.
165 > */
166 > export function buildCompareTurnsChangesetUri(sessionUri: URI, originalTurnId: string, modifiedTurnId: string): URI {
167 > if (!originalTurnId || originalTurnId.includes('/')) { changesetUri.ts ×2
168 > throw new Error(`buildCompareTurnsChangesetUri: originalTurnId must be non-empty and not contain '/' (got ${JSON.stringify(originalTurnId)})`); changesetUri.ts ×5
169 > }
170 > if (!modifiedTurnId || modifiedTurnId.includes('/')) { changesetUri.ts ×2
171 > throw new Error(`buildCompareTurnsChangesetUri: modifiedTurnId must be non-empty and not contain '/' (got ${JSON.stringify(modifiedTurnId)})`); changesetUri.ts ×5
172 > }
173 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${COMPARE_CHANGESET_PREFIX}${originalTurnId}/${modifiedTurnId}`; changesetUri.ts ×1
174 > }
176 > /**
177 > * Returns the subscribable URI for an opaque, producer-defined
178 > * `changesetId`. The id must not contain `/` — well-known multi-segment
179 > * shapes have dedicated builders (e.g. {@link buildTurnChangesetUri}).
180 > */
181 > export function buildChangesetUri(sessionUri: URI, changesetId: string): URI {
182 > if (!changesetId) { changesetUri.ts ×2
183 > throw new Error('buildChangesetUri: changesetId must be non-empty'); changesetUri.ts ×5
184 > }
185 > if (changesetId.includes('/')) { changesetUri.ts ×2
186 > throw new Error(`buildChangesetUri: changesetId must not contain '/' (got ${JSON.stringify(changesetId)})`); changesetUri.ts ×5
187 > }
188 > return `${sessionUri}${CHANGESET_PATH_SEGMENT}${changesetId}`; changesetUri.ts ×1
189 > }
191 > /**
192 > * Parses a changeset URI back into `(sessionUri, changesetId, kind)`,
193 > * or returns `undefined` if `uri` is not a changeset URI we recognise.
194 > */
195 > export function parseChangesetUri(uri: URI): { sessionUri: URI; changesetId: string; kind: ChangesetKind; turnId?: string; originalTurnId?: string; modifiedTurnId?: string } | undefined {
196 > const idx = uri.lastIndexOf(CHANGESET_PATH_SEGMENT); changesetUri.ts ×1
197 > if (idx < 0) {
198 > return undefined; changesetUri.ts ×1
199 > }
200 > const changesetId = uri.slice(idx + CHANGESET_PATH_SEGMENT.length); changesetUri.ts ×2
201 > if (!changesetId) {
202 return undefined;
203 }
204 > const sessionUri = uri.slice(0, idx); changesetUri.ts ×2
205 > if (changesetId === BRANCH_CHANGESET_ID) {
206 > return { sessionUri, changesetId, kind: ChangesetKind.Branch }; changesetUri.ts ×1
207 > }
208 > if (changesetId === UNCOMMITTED_CHANGESET_ID) { changesetUri.ts ×1
209 > return { sessionUri, changesetId, kind: ChangesetKind.Uncommitted }; changesetUri.ts ×1
210 > }
211 > if (changesetId === SESSION_CHANGESET_ID) { changesetUri.ts ×1
212 > return { sessionUri, changesetId, kind: ChangesetKind.Session }; changesetUri.ts ×1
213 > }
214 > if (changesetId.startsWith(TURN_CHANGESET_PREFIX)) { changesetUri.ts ×1
215 > const turnId = changesetId.slice(TURN_CHANGESET_PREFIX.length); changesetUri.ts ×1
216 > // Reject the unexpanded template and any tail with extra segments.
217 > if (!turnId || turnId.includes('/') || turnId === TURN_TEMPLATE_VARIABLE) {
218 > return undefined; changesetUri.ts ×1
219 > }
220 > return { sessionUri, changesetId, kind: ChangesetKind.Turn, turnId }; changesetUri.ts ×1
221 > }
222 > if (changesetId.startsWith(COMPARE_CHANGESET_PREFIX)) { changesetUri.ts ×1
223 > const tail = changesetId.slice(COMPARE_CHANGESET_PREFIX.length); changesetUri.ts ×2
224 > const parts = tail.split('/');
225 > // Reject anything that isn't exactly `<originalTurnId>/<modifiedTurnId>`,
226 > // and reject unexpanded template variables on either side.
227 > if (parts.length !== 2) {
228 > return undefined; changesetUri.ts ×2
229 > }
230 > const [originalTurnId, modifiedTurnId] = parts; changesetUri.ts ×2
231 > if (!originalTurnId || !modifiedTurnId
232 > || originalTurnId === COMPARE_ORIGINAL_TEMPLATE_VARIABLE
233 > || modifiedTurnId === COMPARE_MODIFIED_TEMPLATE_VARIABLE) {
234 > return undefined; changesetUri.ts ×1
235 > }
236 > return { sessionUri, changesetId, kind: ChangesetKind.Compare, originalTurnId, modifiedTurnId }; changesetUri.ts ×1
237 > }
238 > if (changesetId.includes('/')) { changesetUri.ts ×1
239 > return undefined; changesetUri.ts ×2
240 > }
241 > return { sessionUri, changesetId, kind: ChangesetKind.Unknown }; changesetUri.ts ×1
242 > }
244 > /** Returns `true` iff `uri` looks like a changeset URI we recognise. */
245 > export function isChangesetUri(uri: URI): boolean {
246 > return parseChangesetUri(uri) !== undefined; changesetUri.ts ×3
247 > }
249 > /** Returns `true` iff `uri` is the session-wide changeset URI. */
250 > export function isSessionChangesetUri(uri: URI): boolean {
251 > return parseChangesetUri(uri)?.kind === ChangesetKind.Session; changesetUri.ts ×3
252 > }
254 > /** Returns `true` iff `uri` is the uncommitted-changes changeset URI. */
255 > export function isUncommittedChangesetUri(uri: URI): boolean {
256 > return parseChangesetUri(uri)?.kind === ChangesetKind.Uncommitted; changesetUri.ts ×3
257 > }
259 > /** Returns the parsed turn id when `uri` is a per-turn changeset URI. */
260 > export function parseTurnChangesetUri(uri: URI): { sessionUri: URI; turnId: string } | undefined {
261 > const parsed = parseChangesetUri(uri); changesetUri.ts ×1
262 > if (parsed?.kind !== ChangesetKind.Turn || parsed.turnId === undefined) {
263 > return undefined;
264 > }
265 > return { sessionUri: parsed.sessionUri, turnId: parsed.turnId };
266 > }
268 > /** Returns the parsed turn ids when `uri` is a compare-turns changeset URI. */
269 > export function parseCompareTurnsChangesetUri(uri: URI): { sessionUri: URI; originalTurnId: string; modifiedTurnId: string } | undefined {
270 > const parsed = parseChangesetUri(uri); changesetUri.ts ×1
271 > if (parsed?.kind !== ChangesetKind.Compare || parsed.originalTurnId === undefined || parsed.modifiedTurnId === undefined) {
272 > return undefined;
273 > }
274 > return { sessionUri: parsed.sessionUri, originalTurnId: parsed.originalTurnId, modifiedTurnId: parsed.modifiedTurnId };
275 > }
277 > /**
278 > * Builds the default ordered `summary.changesets` catalogue for a
279 > * session (`Branch Changes`, `Uncommitted Changes`, `This Turn`) with
280 > * label + uriTemplate only. Aggregate counts are filled in later by the
281 > * diff producer as compute passes complete.
282 > *
283 > * The first two entries (`Branch Changes`, `Uncommitted Changes`) are
284 > * git-only; `AgentService._attachGitState` strips them asynchronously
285 > * for sessions whose working directory is not a git repo. The backing
286 > * per-changeset states are still registered for every session — only
287 > * the catalogue advertisements are stripped.
288 > *
289 > * The compare-turns changeset (built by
290 > * {@link buildCompareTurnsChangesetUri}) is intentionally NOT included
291 > * in the default catalogue: it is subscribe-only. Clients that want
292 > * compare-turns diffs construct the URI themselves from two known
293 > * turn ids and subscribe directly.
294 > */
295 > export function buildDefaultChangesetCatalog(sessionUri: URI, state?: ISessionWithDefaultChat): Changeset[] {
296 > // Session that failed to create agentHostStateManager.ts ×3
297 > if (!state || state.lifecycle === SessionLifecycle.CreationFailed) {
298 > return []; changesetUri.ts ×1
299 > }
301 > // New Session
302 > if (state.lifecycle === SessionLifecycle.Creating) {
303 > if (readSessionWorkspaceless(state._meta)) { agentService.ts ×16
304 > // Quick chat changesetUri.ts ×1
305 > return [];
306 > }
308 > // Uncommitted changes
309 > return [{
310 > label: uncommittedChangesetLabel(),
311 > description: uncommittedChangesetDescription(),
312 > uriTemplate: buildUncommittedChangesetUri(sessionUri),
313 > changeKind: ChangesetKind.Uncommitted
314 > }];
315 > }
317 > const gitState = readSessionGitState(state._meta);
318 >
319 > if (!gitState) {
320 > // No git repository changesetUri.ts ×1
321 > return [{
322 > label: sessionChangesetLabel(),
323 > description: sessionChangesetDescription(),
324 > uriTemplate: buildSessionChangesetUri(sessionUri),
325 > changeKind: ChangesetKind.Session
326 > },
327 > {
328 > label: thisTurnChangesetLabel(),
329 > description: thisTurnChangesetDescription(),
330 > uriTemplate: buildTurnChangesetUriTemplate(sessionUri),
331 > changeKind: ChangesetKind.Turn
332 > }] satisfies Changeset[];
333 > }
335 > return [
336 > {
337 > label: branchChangesetLabel(),
338 > description: gitState
339 > ? formatBranchChangesetDescription(gitState)
340 : undefined,
341 > uriTemplate: buildBranchChangesetUri(sessionUri), agentHostStateManager.ts ×3
342 > changeKind: ChangesetKind.Branch,
343 > capabilities: { review: {} }
344 > },
345 > {
346 > label: uncommittedChangesetLabel(),
347 > description: uncommittedChangesetDescription(),
348 > uriTemplate: buildUncommittedChangesetUri(sessionUri),
349 > changeKind: ChangesetKind.Uncommitted
350 > },
351 > {
352 > label: sessionChangesetLabel(),
353 > description: sessionChangesetDescription(),
354 > uriTemplate: buildSessionChangesetUri(sessionUri),
355 > changeKind: ChangesetKind.Session
356 > },
357 > {
358 > label: thisTurnChangesetLabel(),
359 > description: thisTurnChangesetDescription(),
360 > uriTemplate: buildTurnChangesetUriTemplate(sessionUri),
361 > changeKind: ChangesetKind.Turn
362 > },
363 > {
364 > label: compareTurnsChangesetLabel(),
365 > description: compareTurnsChangesetDescription(),
366 > uriTemplate: buildCompareTurnsChangesetUriTemplate(sessionUri),
367 > changeKind: ChangesetKind.Compare
368 > }
369 > ] satisfies Changeset[];
370 > }