jupyterlab_ai_code_assistants_extension 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +44 -0
  3. package/lib/core/colour.d.ts +57 -0
  4. package/lib/core/colour.js +190 -0
  5. package/lib/core/icons.d.ts +12 -0
  6. package/lib/core/icons.js +99 -0
  7. package/lib/core/modes.d.ts +13 -0
  8. package/lib/core/modes.js +36 -0
  9. package/lib/core/panel.d.ts +233 -0
  10. package/lib/core/panel.js +1673 -0
  11. package/lib/core/popup.d.ts +40 -0
  12. package/lib/core/popup.js +404 -0
  13. package/lib/core/registry.d.ts +24 -0
  14. package/lib/core/registry.js +48 -0
  15. package/lib/core/request.d.ts +25 -0
  16. package/lib/core/request.js +64 -0
  17. package/lib/core/terminals.d.ts +129 -0
  18. package/lib/core/terminals.js +344 -0
  19. package/lib/core/types.d.ts +269 -0
  20. package/lib/core/types.js +6 -0
  21. package/lib/index.d.ts +3 -0
  22. package/lib/index.js +277 -0
  23. package/lib/providers/claude.d.ts +3 -0
  24. package/lib/providers/claude.js +88 -0
  25. package/lib/providers/codex.d.ts +3 -0
  26. package/lib/providers/codex.js +79 -0
  27. package/lib/providers/gemini.d.ts +2 -0
  28. package/lib/providers/gemini.js +73 -0
  29. package/lib/providers/index.d.ts +3 -0
  30. package/lib/providers/index.js +16 -0
  31. package/lib/providers/kimi.d.ts +23 -0
  32. package/lib/providers/kimi.js +139 -0
  33. package/package.json +169 -0
  34. package/schema/plugin.json +93 -0
  35. package/src/__tests__/colour.spec.ts +153 -0
  36. package/src/__tests__/core-neutrality.spec.ts +86 -0
  37. package/src/__tests__/labels.spec.ts +89 -0
  38. package/src/__tests__/launch-mode.spec.ts +53 -0
  39. package/src/__tests__/registry.spec.ts +178 -0
  40. package/src/core/colour.ts +224 -0
  41. package/src/core/icons.ts +121 -0
  42. package/src/core/modes.ts +45 -0
  43. package/src/core/panel.ts +2020 -0
  44. package/src/core/popup.ts +475 -0
  45. package/src/core/registry.ts +60 -0
  46. package/src/core/request.ts +96 -0
  47. package/src/core/terminals.ts +446 -0
  48. package/src/core/types.ts +305 -0
  49. package/src/index.ts +356 -0
  50. package/src/providers/claude.ts +102 -0
  51. package/src/providers/codex.ts +89 -0
  52. package/src/providers/gemini.ts +80 -0
  53. package/src/providers/index.ts +21 -0
  54. package/src/providers/kimi.ts +155 -0
  55. package/style/base.css +888 -0
  56. package/style/index.css +1 -0
  57. package/style/index.js +1 -0
@@ -0,0 +1,129 @@
1
+ import { JupyterFrontEnd } from '@jupyterlab/application';
2
+ import { Dialog } from '@jupyterlab/apputils';
3
+ import { ServerConnection } from '@jupyterlab/services';
4
+ import { ITerminalTracker } from '@jupyterlab/terminal';
5
+ import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
6
+ import { ColourStore } from './colour';
7
+ import { ILaunchRequest, IProviderDescriptor, ISession, ITerminalProbeResponse } from './types';
8
+ /** Show a modal spinner while a terminal is being launched. The caller
9
+ * dismisses it with `.dispose()` when the launch settles.
10
+ *
11
+ * Cancel does not abort the request - it only takes the app-wide scrim away, so
12
+ * a stalled launch cannot leave the user staring at a modal with no exit. If
13
+ * the launch later succeeds the terminal simply appears. */
14
+ export declare function showLaunchSpinner(title: string): Dialog<unknown>;
15
+ /**
16
+ * Terminals for one provider: the reuse ladder, the launch call and the tab
17
+ * tint pass.
18
+ *
19
+ * Split out of the panel because none of it is presentation - it is process
20
+ * identity. The panel asks for a conversation and gets a focused terminal
21
+ * running exactly that conversation, or a fresh one; nothing in here knows
22
+ * about sections, filters or menus.
23
+ */
24
+ export declare class TerminalManager {
25
+ constructor(options: TerminalManager.IOptions);
26
+ dispose(): void;
27
+ /**
28
+ * Open a conversation, reusing an open terminal only when that terminal is
29
+ * POSITIVELY running it.
30
+ *
31
+ * Three steps, cheapest first: the per-project microcache, then a walk of
32
+ * every terminal JupyterLab knows about (each probed server-side for the
33
+ * conversation its process actually holds), then a fresh launch. A terminal
34
+ * whose conversation cannot be read is never reused - it may be running a
35
+ * different conversation of the same project, and a session id is globally
36
+ * unique, so an unreadable terminal can never be proven to be the wanted one.
37
+ *
38
+ * Concurrent clicks on the SAME conversation attach to the in-flight
39
+ * promise; a different conversation of the same project launches
40
+ * independently, so branches open side by side.
41
+ */
42
+ openSession(session: ISession, mode: string | undefined, sessionId?: string): Promise<void>;
43
+ private _doOpenSession;
44
+ /**
45
+ * Launch a terminal for one request and focus it. The returned widget is
46
+ * tagged with `tagSessionId` in the microcache, so a later click on the row
47
+ * that conversation now belongs to reuses this very terminal.
48
+ */
49
+ launch(request: ILaunchRequest, tagSessionId?: string): Promise<any>;
50
+ /**
51
+ * Bring a terminal tab to the front AND hand it keyboard focus, so typing
52
+ * starts without a second click. `activateById` only reveals the tab; the
53
+ * xterm inside does not reliably take DOM focus when the click came from the
54
+ * sidebar, so the focus call is deferred to the next frame - by then the
55
+ * widget is attached and visible.
56
+ */
57
+ focus(widget: any): void;
58
+ /** Find an open terminal running exactly this conversation, or null.
59
+ *
60
+ * Matches on the conversation id ALONE. The server reads the true id from
61
+ * the running process, so a session the user started by hand is recognised
62
+ * too, not only launches that carry an argv id. An id is globally unique to
63
+ * one conversation, so a terminal holding it IS the one to focus, whatever
64
+ * cwd it reports - a process that changed directory, or whose project dir
65
+ * was recreated, must still be reused rather than duplicated. */
66
+ findForSession(wantedSessionId: string | undefined): Promise<{
67
+ widget: any;
68
+ runningId: string | null;
69
+ } | null>;
70
+ /** Ask the server what a terminal is running. Null when the widget has no
71
+ * session name yet, or the probe fails - a terminal can vanish between
72
+ * enumeration and probe, which is a retry, not an error. */
73
+ probe(widget: any): Promise<ITerminalProbeResponse | null>;
74
+ /** Turn tinting on or off. Off drops the tint from this provider's terminals
75
+ * at once rather than at the next reload; on re-tints from current state. */
76
+ setColouredTabs(on: boolean): void;
77
+ /** Remember the sessions the last poll returned, so the colour loop can run
78
+ * on its own timer without asking the panel for state. */
79
+ setSessions(sessions: ISession[]): void;
80
+ /** Arm the colour loop. Independent of panel visibility: a terminal running
81
+ * the assistant is tinted whether or not the panel is on screen. */
82
+ startColourLoop(): void;
83
+ stopColourLoop(): void;
84
+ /**
85
+ * Re-tint every open terminal of this assistant from the freshest colours,
86
+ * whether this panel launched it or the user opened it themselves.
87
+ *
88
+ * Each terminal's conversation is re-resolved on every pass rather than
89
+ * taken from the launch cache, which pins the launch-time conversation and
90
+ * goes stale on an in-place switch. On the way through, a colour the user
91
+ * set by hand on the tab is written back into this provider's colour store,
92
+ * which is what makes "set the tab colour" mean "this conversation's colour"
93
+ * even for an assistant whose CLI has no colour concept.
94
+ */
95
+ reconcileColours(): Promise<void>;
96
+ /** Effective colour of one conversation right now, for branch inheritance
97
+ * at the fork sites. */
98
+ colourFor(session: ISession, sessionId?: string): string | null;
99
+ /** Drop the tint from this assistant's terminals. Only its own terminals are
100
+ * touched, so a tint the user set by hand on another tab survives. */
101
+ clearColours(): Promise<void>;
102
+ private _applyColour;
103
+ private _eachAssistantTerminal;
104
+ private _liveTerminals;
105
+ private _wireDisposal;
106
+ private _key;
107
+ private readonly _app;
108
+ private readonly _descriptor;
109
+ private readonly _colours;
110
+ private readonly _tracker;
111
+ private readonly _colourfulTabs;
112
+ private readonly _serverSettings;
113
+ private readonly _byProject;
114
+ private readonly _pending;
115
+ private _sessions;
116
+ private _colouredTabs;
117
+ private _colourHandle;
118
+ private _disposed;
119
+ }
120
+ export declare namespace TerminalManager {
121
+ interface IOptions {
122
+ app: JupyterFrontEnd;
123
+ descriptor: IProviderDescriptor;
124
+ colourStore: ColourStore;
125
+ terminalTracker: ITerminalTracker | null;
126
+ colourfulTabs: IColourfulTabs | null;
127
+ serverSettings: ServerConnection.ISettings;
128
+ }
129
+ }
@@ -0,0 +1,344 @@
1
+ import { Dialog } from '@jupyterlab/apputils';
2
+ import { Widget } from '@lumino/widgets';
3
+ import { effectiveColour, readUserSetTabColour, sessionForCwds } from './colour';
4
+ import { requestProvider } from './request';
5
+ /** Colour reconciliation cadence. Matches the row poll so the two collapse
6
+ * into one rhythm while the panel is visible, but it is a separate timer: a
7
+ * terminal running the assistant is tinted whether or not the panel is open. */
8
+ const COLOUR_INTERVAL_MS = 30000;
9
+ /** Show a modal spinner while a terminal is being launched. The caller
10
+ * dismisses it with `.dispose()` when the launch settles.
11
+ *
12
+ * Cancel does not abort the request - it only takes the app-wide scrim away, so
13
+ * a stalled launch cannot leave the user staring at a modal with no exit. If
14
+ * the launch later succeeds the terminal simply appears. */
15
+ export function showLaunchSpinner(title) {
16
+ const body = new Widget();
17
+ body.node.className = 'jp-AiAssistantsPanel-launchOverlay';
18
+ const spinner = document.createElement('div');
19
+ spinner.className =
20
+ 'jp-aiAssistants-spinner jp-AiAssistantsPanel-launchSpinner';
21
+ body.node.appendChild(spinner);
22
+ const dialog = new Dialog({
23
+ title,
24
+ body,
25
+ buttons: [Dialog.cancelButton()]
26
+ });
27
+ // `launch()` returns a promise nobody awaits - disposing an open Lumino
28
+ // dialog rejects it with `undefined`, so swallow that here and keep a benign
29
+ // teardown from surfacing as an unhandled rejection.
30
+ dialog.launch().catch(() => undefined);
31
+ return dialog;
32
+ }
33
+ /**
34
+ * Terminals for one provider: the reuse ladder, the launch call and the tab
35
+ * tint pass.
36
+ *
37
+ * Split out of the panel because none of it is presentation - it is process
38
+ * identity. The panel asks for a conversation and gets a focused terminal
39
+ * running exactly that conversation, or a fresh one; nothing in here knows
40
+ * about sections, filters or menus.
41
+ */
42
+ export class TerminalManager {
43
+ constructor(options) {
44
+ // Most recent terminal per project, tagged with the conversation it runs so
45
+ // reuse can tell a project's branches apart.
46
+ this._byProject = new Map();
47
+ // In-flight launches keyed per CONVERSATION, so two branches of one project
48
+ // open independently and concurrently.
49
+ this._pending = new Map();
50
+ this._sessions = [];
51
+ this._colouredTabs = true;
52
+ this._colourHandle = null;
53
+ this._disposed = false;
54
+ this._app = options.app;
55
+ this._descriptor = options.descriptor;
56
+ this._colours = options.colourStore;
57
+ this._tracker = options.terminalTracker;
58
+ this._colourfulTabs = options.colourfulTabs;
59
+ this._serverSettings = options.serverSettings;
60
+ }
61
+ dispose() {
62
+ this._disposed = true;
63
+ this.stopColourLoop();
64
+ }
65
+ /**
66
+ * Open a conversation, reusing an open terminal only when that terminal is
67
+ * POSITIVELY running it.
68
+ *
69
+ * Three steps, cheapest first: the per-project microcache, then a walk of
70
+ * every terminal JupyterLab knows about (each probed server-side for the
71
+ * conversation its process actually holds), then a fresh launch. A terminal
72
+ * whose conversation cannot be read is never reused - it may be running a
73
+ * different conversation of the same project, and a session id is globally
74
+ * unique, so an unreadable terminal can never be proven to be the wanted one.
75
+ *
76
+ * Concurrent clicks on the SAME conversation attach to the in-flight
77
+ * promise; a different conversation of the same project launches
78
+ * independently, so branches open side by side.
79
+ */
80
+ async openSession(session, mode, sessionId) {
81
+ const wanted = sessionId !== null && sessionId !== void 0 ? sessionId : session.session_id;
82
+ const key = this._key(session.project_path, wanted);
83
+ const inFlight = this._pending.get(key);
84
+ if (inFlight) {
85
+ return inFlight;
86
+ }
87
+ const promise = this._doOpenSession(session, mode, wanted).finally(() => {
88
+ this._pending.delete(key);
89
+ });
90
+ this._pending.set(key, promise);
91
+ return promise;
92
+ }
93
+ async _doOpenSession(session, mode, wanted) {
94
+ var _a;
95
+ // 1. Microcache - the most recent terminal of this project, reused only
96
+ // when it carries the wanted conversation.
97
+ const cached = this._byProject.get(session.project_path);
98
+ if (cached && !cached.widget.isDisposed && cached.sessionId === wanted) {
99
+ this.focus(cached.widget);
100
+ return;
101
+ }
102
+ // 2. Walk every live terminal, asking the server what each is running.
103
+ const found = await this.findForSession(wanted);
104
+ if (found) {
105
+ // Tag with the OBSERVED conversation, so a later reuse trusts a
106
+ // confirmed identity rather than a wish.
107
+ this._byProject.set(session.project_path, {
108
+ widget: found.widget,
109
+ sessionId: (_a = found.runningId) !== null && _a !== void 0 ? _a : undefined
110
+ });
111
+ this._wireDisposal(session.project_path, found.widget);
112
+ this.focus(found.widget);
113
+ return;
114
+ }
115
+ // 3. Fresh launch. The server picks the verb (resume, attach, bare) at
116
+ // launch time, because ownership of a conversation can change inside the
117
+ // poll window and only the server can be authoritative about it.
118
+ await this.launch({
119
+ project_path: session.project_path,
120
+ encoded_path: session.encoded_path,
121
+ session_id: wanted,
122
+ mode
123
+ }, wanted);
124
+ }
125
+ /**
126
+ * Launch a terminal for one request and focus it. The returned widget is
127
+ * tagged with `tagSessionId` in the microcache, so a later click on the row
128
+ * that conversation now belongs to reuses this very terminal.
129
+ */
130
+ async launch(request, tagSessionId) {
131
+ const spinner = showLaunchSpinner(`Opening ${this._descriptor.label}`);
132
+ try {
133
+ const launched = await requestProvider(this._descriptor.id, 'launch', this._serverSettings, { method: 'POST', body: JSON.stringify(request) });
134
+ const widget = await this._app.commands.execute('terminal:open', {
135
+ name: launched.terminal_name
136
+ });
137
+ if (widget === null || widget === void 0 ? void 0 : widget.id) {
138
+ this._byProject.set(request.project_path, {
139
+ widget,
140
+ sessionId: tagSessionId
141
+ });
142
+ this._wireDisposal(request.project_path, widget);
143
+ this.focus(widget);
144
+ }
145
+ return widget !== null && widget !== void 0 ? widget : null;
146
+ }
147
+ finally {
148
+ spinner.dispose();
149
+ }
150
+ }
151
+ /**
152
+ * Bring a terminal tab to the front AND hand it keyboard focus, so typing
153
+ * starts without a second click. `activateById` only reveals the tab; the
154
+ * xterm inside does not reliably take DOM focus when the click came from the
155
+ * sidebar, so the focus call is deferred to the next frame - by then the
156
+ * widget is attached and visible.
157
+ */
158
+ focus(widget) {
159
+ if (!widget || widget.isDisposed) {
160
+ return;
161
+ }
162
+ this._app.shell.activateById(widget.id);
163
+ requestAnimationFrame(() => {
164
+ var _a, _b, _c;
165
+ try {
166
+ (_c = (_b = (_a = widget.content) === null || _a === void 0 ? void 0 : _a.term) === null || _b === void 0 ? void 0 : _b.focus) === null || _c === void 0 ? void 0 : _c.call(_b);
167
+ }
168
+ catch (_err) {
169
+ // Disposed in the meantime - nothing to focus.
170
+ }
171
+ });
172
+ }
173
+ /** Find an open terminal running exactly this conversation, or null.
174
+ *
175
+ * Matches on the conversation id ALONE. The server reads the true id from
176
+ * the running process, so a session the user started by hand is recognised
177
+ * too, not only launches that carry an argv id. An id is globally unique to
178
+ * one conversation, so a terminal holding it IS the one to focus, whatever
179
+ * cwd it reports - a process that changed directory, or whose project dir
180
+ * was recreated, must still be reused rather than duplicated. */
181
+ async findForSession(wantedSessionId) {
182
+ if (!this._tracker || !wantedSessionId) {
183
+ return null;
184
+ }
185
+ for (const widget of this._liveTerminals()) {
186
+ const info = await this.probe(widget);
187
+ if (info && info.session_id && info.session_id === wantedSessionId) {
188
+ return { widget, runningId: info.session_id };
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+ /** Ask the server what a terminal is running. Null when the widget has no
194
+ * session name yet, or the probe fails - a terminal can vanish between
195
+ * enumeration and probe, which is a retry, not an error. */
196
+ async probe(widget) {
197
+ var _a, _b;
198
+ const name = (_b = (_a = widget === null || widget === void 0 ? void 0 : widget.content) === null || _a === void 0 ? void 0 : _a.session) === null || _b === void 0 ? void 0 : _b.name;
199
+ if (typeof name !== 'string' || !name) {
200
+ return null;
201
+ }
202
+ try {
203
+ return await requestProvider(this._descriptor.id, `terminal/${encodeURIComponent(name)}`, this._serverSettings);
204
+ }
205
+ catch (_err) {
206
+ return null;
207
+ }
208
+ }
209
+ // --------------------------------------------------------------- colours
210
+ /** Turn tinting on or off. Off drops the tint from this provider's terminals
211
+ * at once rather than at the next reload; on re-tints from current state. */
212
+ setColouredTabs(on) {
213
+ const enabled = !!on;
214
+ if (this._colouredTabs === enabled) {
215
+ return;
216
+ }
217
+ this._colouredTabs = enabled;
218
+ void (enabled ? this.reconcileColours() : this.clearColours());
219
+ }
220
+ /** Remember the sessions the last poll returned, so the colour loop can run
221
+ * on its own timer without asking the panel for state. */
222
+ setSessions(sessions) {
223
+ this._sessions = sessions;
224
+ }
225
+ /** Arm the colour loop. Independent of panel visibility: a terminal running
226
+ * the assistant is tinted whether or not the panel is on screen. */
227
+ startColourLoop() {
228
+ if (this._disposed || this._colourHandle !== null) {
229
+ return;
230
+ }
231
+ this._colourHandle = window.setTimeout(() => {
232
+ this._colourHandle = null;
233
+ void this.reconcileColours();
234
+ }, COLOUR_INTERVAL_MS);
235
+ }
236
+ stopColourLoop() {
237
+ if (this._colourHandle !== null) {
238
+ window.clearTimeout(this._colourHandle);
239
+ this._colourHandle = null;
240
+ }
241
+ }
242
+ /**
243
+ * Re-tint every open terminal of this assistant from the freshest colours,
244
+ * whether this panel launched it or the user opened it themselves.
245
+ *
246
+ * Each terminal's conversation is re-resolved on every pass rather than
247
+ * taken from the launch cache, which pins the launch-time conversation and
248
+ * goes stale on an in-place switch. On the way through, a colour the user
249
+ * set by hand on the tab is written back into this provider's colour store,
250
+ * which is what makes "set the tab colour" mean "this conversation's colour"
251
+ * even for an assistant whose CLI has no colour concept.
252
+ */
253
+ async reconcileColours() {
254
+ if (!this._colouredTabs) {
255
+ // No rearm: `setColouredTabs(true)` calls straight back in here.
256
+ return;
257
+ }
258
+ try {
259
+ await this._eachAssistantTerminal(async (widget, info) => {
260
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
261
+ const sessionId = (_d = (_a = info.session_id) !== null && _a !== void 0 ? _a : (_c = sessionForCwds((_b = info.cwds) !== null && _b !== void 0 ? _b : [], this._sessions)) === null || _c === void 0 ? void 0 : _c.session_id) !== null && _d !== void 0 ? _d : null;
262
+ const terminalName = (_g = (_f = (_e = widget === null || widget === void 0 ? void 0 : widget.content) === null || _e === void 0 ? void 0 : _e.session) === null || _f === void 0 ? void 0 : _f.name) !== null && _g !== void 0 ? _g : '';
263
+ // Write-back. Native-colour assistants own their colour, so their
264
+ // tabs are read-only here; for the others the tab IS the control
265
+ // surface.
266
+ if (sessionId &&
267
+ terminalName &&
268
+ this._descriptor.colourSource !== 'native') {
269
+ const userSet = readUserSetTabColour(terminalName);
270
+ if (userSet && userSet !== this._colours.get(sessionId)) {
271
+ await this._colours.set(sessionId, userSet);
272
+ }
273
+ }
274
+ const native = (_h = info.colour) !== null && _h !== void 0 ? _h : (sessionId
275
+ ? ((_k = (_j = this._sessions.find(s => s.session_id === sessionId)) === null || _j === void 0 ? void 0 : _j.colour) !== null && _k !== void 0 ? _k : null)
276
+ : null);
277
+ this._applyColour(widget, effectiveColour(sessionId, this._descriptor.colourSource, this._colours.get(sessionId), native));
278
+ });
279
+ }
280
+ finally {
281
+ // Always rearm, even after a failed pass - one bad probe must not end
282
+ // tinting for the rest of the session.
283
+ this.startColourLoop();
284
+ }
285
+ }
286
+ /** Effective colour of one conversation right now, for branch inheritance
287
+ * at the fork sites. */
288
+ colourFor(session, sessionId) {
289
+ var _a;
290
+ const id = sessionId !== null && sessionId !== void 0 ? sessionId : session.session_id;
291
+ return effectiveColour(id, this._descriptor.colourSource, this._colours.get(id), (_a = session.colour) !== null && _a !== void 0 ? _a : null);
292
+ }
293
+ /** Drop the tint from this assistant's terminals. Only its own terminals are
294
+ * touched, so a tint the user set by hand on another tab survives. */
295
+ async clearColours() {
296
+ await this._eachAssistantTerminal(async (widget) => {
297
+ this._applyColour(widget, null);
298
+ });
299
+ }
300
+ _applyColour(widget, colourId) {
301
+ if (!this._colourfulTabs || !widget || widget.isDisposed) {
302
+ return;
303
+ }
304
+ this._colourfulTabs.setColour(widget, colourId);
305
+ }
306
+ async _eachAssistantTerminal(apply) {
307
+ if (!this._colourfulTabs || !this._tracker) {
308
+ return;
309
+ }
310
+ const widgets = this._liveTerminals();
311
+ await Promise.all(widgets.map(async (widget) => {
312
+ const info = await this.probe(widget);
313
+ if (!info || !info.running || widget.isDisposed) {
314
+ return;
315
+ }
316
+ await apply(widget, info);
317
+ }));
318
+ }
319
+ _liveTerminals() {
320
+ var _a;
321
+ const widgets = [];
322
+ (_a = this._tracker) === null || _a === void 0 ? void 0 : _a.forEach((widget) => {
323
+ if (widget && !widget.isDisposed) {
324
+ widgets.push(widget);
325
+ }
326
+ });
327
+ return widgets;
328
+ }
329
+ _wireDisposal(projectPath, widget) {
330
+ var _a;
331
+ if (!((_a = widget === null || widget === void 0 ? void 0 : widget.disposed) === null || _a === void 0 ? void 0 : _a.connect)) {
332
+ return;
333
+ }
334
+ widget.disposed.connect(() => {
335
+ var _a;
336
+ if (((_a = this._byProject.get(projectPath)) === null || _a === void 0 ? void 0 : _a.widget) === widget) {
337
+ this._byProject.delete(projectPath);
338
+ }
339
+ });
340
+ }
341
+ _key(projectPath, sessionId) {
342
+ return `${projectPath}\n${sessionId !== null && sessionId !== void 0 ? sessionId : ''}`;
343
+ }
344
+ }