claudeSessionClientCustomizationsModel.ts ×14

Frontier kind: Code frontier

unlabeled · c_a6c3214e7aa5

208 tests · 13424 LOC · 77 files · introduces 0 tests · 123 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
14 ranges123 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1112 ranges13424 lines · 77 files · Browse complete extent
All tests (intent)
208 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.

1 file ranked by introduced lines: 123 introduced LOC across 14 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/claude/customizations/claudeSessionClientCustomizationsModel.ts 123 introduced LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeSessionClientCustomizationsModel.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 { Event } from '../../../../../base/common/event.js';
7 > import { Disposable } from '../../../../../base/common/lifecycle.js';
8 > import { equals as arraysEqual } from '../../../../../base/common/arrays.js';
9 > import { URI } from '../../../../../base/common/uri.js';
10 > import { autorun, IObservable, ISettableObservable, observableValueOpts } from '../../../../../base/common/observable.js';
11 > import type { ISyncedCustomization } from '../../../common/agentPluginManager.js';
12 > import type { ClientPluginCustomization } from '../../../common/state/sessionState.js';
13 >
14 > /**
15 > * Per-session **client-pushed** customization snapshot. Server-side
16 > * (SDK-discovered) customizations live separately and are never stored here.
17 > */
18 > export interface ISessionCustomizationsState {
19 > readonly synced: readonly ISyncedCustomization[];
20 > }
21 >
22 > const INITIAL_STATE: ISessionCustomizationsState = { synced: [] };
23 >
24 > /**
25 > * Pure observable state holder for the **client-pushed**
26 > * {@link ISyncedCustomization} list.
27 > *
28 > * Server-side (SDK-discovered) customizations are NOT in scope here
29 > * — they're fetched on demand from the live `Query` in
30 > * `getSessionCustomizations` and never written into this
31 > * model.
32 > *
33 > * `state` dedupes structurally-equivalent writes: a re-send of the
34 > * same synced snapshot does NOT fire downstream
35 > * subscribers. Knows nothing about diffing or the SDK — pair with
36 > * {@link SessionClientCustomizationsDiff} to track "has the client-pushed
37 > * snapshot changed since the last successful SDK plugin reload".
38 > */
39 > export class SessionClientCustomizationsModel {
40
41 /** Per-client synced customizations, keyed by `clientId`, merged into `state.synced`. */
47 );
48 readonly state: IObservable<ISessionCustomizationsState> = this._state;
50 > /**
51 > * The union of every client's synced customizations, deduplicated by
52 > * customization `id` with the first-inserted client winning. Order
53 > * follows client insertion order.
54 > */
55 > private _mergedSynced(): readonly ISyncedCustomization[] {
56 const seen = new Set<string>();
57 const result: ISyncedCustomization[] = [];
67 return result;
68 }
70 > /** Replace a single client's pushed customization snapshot for this session. */
71 > setSyncedCustomizations(clientId: string, synced: readonly ISyncedCustomization[]): void {
72 this._byClient.set(clientId, synced);
73 this._state.set({ synced: this._mergedSynced() }, undefined);
74 }
76 > /** Remove a client's pushed customizations from this session. */
77 > removeClient(clientId: string): void {
78 if (!this._byClient.delete(clientId)) {
79 return;
81 this._state.set({ synced: this._mergedSynced() }, undefined);
82 }
84 >
85 > /**
86 > * Tracks "has the **client-pushed** customization snapshot changed
87 > * since the SDK was last (re)started against it?". Subscribes to
88 > * {@link SessionClientCustomizationsModel.state}, with the state
89 > * observable's equalsFn structurally comparing the meaningful
90 > * fields (URI list, nonce, status, user-visible
91 > * metadata). Same race semantics as `SessionClientToolsDiff`: a
92 > * write that lands during an in-flight rebind re-flips dirty via
93 > * the autorun, so callers don't need to snapshot-compare.
94 > *
95 > * The SDK captures `Options.plugins` at startup. Synced customization changes
96 > * mark the diff dirty, while reducer-backed enablement drift is detected by
97 > * comparing desired plugin paths with the last successfully applied paths.
98 > *
99 > * Server-side (SDK-discovered) customizations are NOT tracked
100 > * here — the SDK manages its own discovery lifecycle, and
101 > * changes to server-side data flow to the workbench via separate
102 > * event fires (post-materialize, post-rebind).
103 > *
104 > * On rebind throw the bit is left set — the SDK is still running
105 > * with the previous plugin set, so the next sendMessage should
106 > * retry.
107 > */
108 > export class SessionClientCustomizationsDiff extends Disposable {
109 >
110 > readonly model: SessionClientCustomizationsModel = new SessionClientCustomizationsModel();
111 >
112 > private _dirty = false;
113 > private _appliedPluginPaths: readonly URI[] = [];
114 > // `autorun` invokes its callback once at registration for dependency
115 > // tracking. Skip that initial run so a brand-new diff doesn't
116 > // report dirty before any mutation has happened.
117 > private _ignoreNextFire = true;
118 >
119 > /**
120 > * Outward fire-and-forget signal that the underlying state
121 > * changed. Derived from the observable so external listeners
122 > * (e.g. agent-level event aggregation) don't have to subscribe to
123 > * the observable directly.
124 > */
125 > readonly onDidChange: Event<void> = Event.fromObservableLight(this.model.state);
126 >
127 > constructor() {
128 super();
129 this._register(autorun(reader => {
136 }));
137 }
139 > get hasDifference(): boolean {
140 return this._dirty;
141 }
143 > hasDifferenceFrom(pluginPaths: readonly URI[]): boolean {
144 return this._dirty || !pluginPathsEqual(this._appliedPluginPaths, pluginPaths);
145 }
147 > /**
148 > * Record the resolved desired plugin paths and mark the current
149 > * snapshot as applied. A subsequent write that changes any
150 > * meaningful field re-flips dirty via the autorun. If the caller's
151 > * downstream work (e.g. SDK rebind) fails, call {@link markDirty}
152 > * to surface the stale state.
153 > */
154 > consume(paths: readonly URI[]): readonly URI[] {
155 this._appliedPluginPaths = paths;
156 this._dirty = false;
157 return paths;
158 }
160 > /**
161 > * Force the dirty bit on. Use when async work that followed
162 > * {@link consume} failed and the SDK is therefore still on the
163 > * previous plugin set.
164 > */
165 > markDirty(): void {
166 this._dirty = true;
167 }
169 >
170 function stateEqual(a: ISessionCustomizationsState, b: ISessionCustomizationsState): boolean {
171 return syncedListEqual(a.synced, b.synced);
172 }
174 function syncedListEqual(a: readonly ISyncedCustomization[], b: readonly ISyncedCustomization[]): boolean {
175 if (a.length !== b.length) {
209 return true;
210 }
212 function loadMessageOf(load: { kind: string; message?: string } | undefined): string | undefined {
213 return load && load.message ? load.message : undefined;
214 }
216 function childrenEqual(a: readonly { id: string; name: string }[] | undefined, b: readonly { id: string; name: string }[] | undefined): boolean {
217 if (a === b) {
228 return true;
229 }
231 function pluginPathsEqual(a: readonly URI[], b: readonly URI[]): boolean {
232 return arraysEqual(a, b, (x, y) => x.toString() === y.toString());