xtralab 0.5.0 → 0.7.0
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.
- package/README.md +138 -76
- package/lib/agentSessions.d.ts +50 -0
- package/lib/agentSessions.js +45 -0
- package/lib/fileBrowser/index.js +5 -1
- package/lib/git/imageDiff.js +80 -8
- package/lib/index.d.ts +3 -2
- package/lib/index.js +11 -6
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +22 -13
- package/lib/launcher/commands.d.ts +32 -6
- package/lib/launcher/commands.js +81 -57
- package/lib/launcher/dashboard.d.ts +15 -0
- package/lib/launcher/dashboard.js +139 -10
- package/lib/launcher/editorRegistry.d.ts +43 -0
- package/lib/launcher/editorRegistry.js +96 -0
- package/lib/launcher/editors.d.ts +78 -0
- package/lib/launcher/editors.js +110 -0
- package/lib/launcher/icons.d.ts +25 -6
- package/lib/launcher/icons.js +116 -19
- package/lib/launcher/index.d.ts +2 -22
- package/lib/launcher/index.js +48 -25
- package/lib/launcher/registry.d.ts +21 -0
- package/lib/launcher/registry.js +28 -0
- package/lib/launcher/tokens.d.ts +41 -0
- package/lib/launcher/tokens.js +17 -0
- package/lib/terminals/detection.d.ts +11 -0
- package/lib/terminals/detection.js +50 -0
- package/lib/terminals/index.d.ts +27 -0
- package/lib/terminals/index.js +194 -0
- package/lib/terminals/model.d.ts +191 -0
- package/lib/terminals/model.js +418 -0
- package/lib/terminals/widget.d.ts +72 -0
- package/lib/terminals/widget.js +93 -0
- package/lib/topBar/icons.d.ts +13 -0
- package/lib/topBar/icons.js +27 -0
- package/lib/topBar/index.d.ts +16 -0
- package/lib/topBar/index.js +98 -0
- package/package.json +36 -33
- package/schema/launcher.json +56 -1
- package/src/agentSessions.ts +94 -0
- package/src/fileBrowser/index.ts +5 -1
- package/src/git/imageDiff.tsx +127 -26
- package/src/index.ts +11 -6
- package/src/launcher/agents.ts +23 -14
- package/src/launcher/commands.ts +98 -60
- package/src/launcher/dashboard.tsx +227 -9
- package/src/launcher/editorRegistry.ts +149 -0
- package/src/launcher/editors.ts +183 -0
- package/src/launcher/icons.ts +120 -20
- package/src/launcher/index.ts +62 -27
- package/src/launcher/registry.ts +33 -0
- package/src/launcher/tokens.ts +51 -0
- package/src/terminals/detection.ts +65 -0
- package/src/terminals/index.ts +222 -0
- package/src/terminals/model.ts +497 -0
- package/src/terminals/widget.tsx +240 -0
- package/src/topBar/icons.ts +30 -0
- package/src/topBar/index.ts +134 -0
- package/style/git.css +109 -0
- package/style/index.css +2 -1
- package/style/index.js +2 -1
- package/style/launcher.css +124 -2
- package/style/terminals.css +235 -0
- package/style/topBar.css +38 -0
- package/lib/statusBar/index.d.ts +0 -14
- package/lib/statusBar/index.js +0 -89
- package/lib/statusBar/widget.d.ts +0 -121
- package/lib/statusBar/widget.js +0 -237
- package/src/statusBar/index.ts +0 -115
- package/src/statusBar/widget.tsx +0 -318
- package/style/statusBar.css +0 -95
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import type { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import type { MainAreaWidget } from '@jupyterlab/apputils';
|
|
3
|
+
import type { ServiceManager, Terminal } from '@jupyterlab/services';
|
|
4
|
+
import type { ITerminal, ITerminalTracker } from '@jupyterlab/terminal';
|
|
5
|
+
import type { IDisposable } from '@lumino/disposable';
|
|
6
|
+
import { Poll } from '@lumino/polling';
|
|
7
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
8
|
+
import type { Title, Widget } from '@lumino/widgets';
|
|
9
|
+
|
|
10
|
+
import type { IAgentSessions } from '../agentSessions';
|
|
11
|
+
import { fetchRunningAgents } from './detection';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The widget shape every entry in `ITerminalTracker` takes — kept here
|
|
15
|
+
* so the registry's call sites read cleanly.
|
|
16
|
+
*/
|
|
17
|
+
export type TerminalWidget = MainAreaWidget<ITerminal.ITerminal>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How often to ask the server which agent (if any) is running in each
|
|
21
|
+
* terminal. Snappy enough that a manually-started agent's logo shows up
|
|
22
|
+
* within a few seconds, light enough that walking the shells' process trees
|
|
23
|
+
* stays negligible.
|
|
24
|
+
*/
|
|
25
|
+
const DETECT_POLL_INTERVAL_MS = 3000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Upper bound on the exponential backoff when detection fails repeatedly
|
|
29
|
+
* (e.g. the endpoint is missing on an older server). Matches the rest of the
|
|
30
|
+
* plugin's polls.
|
|
31
|
+
*/
|
|
32
|
+
const DETECT_POLL_MAX_MS = 300_000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Grace window after a session first appears during which an optimistic
|
|
36
|
+
* launch tag outranks a `null` detection result. It covers the gap between
|
|
37
|
+
* issuing an agent's command and its process actually being spawnable, so the
|
|
38
|
+
* freshly-launched logo doesn't blink off if a detection poll lands in that
|
|
39
|
+
* sliver. Comfortably longer than a cold agent start; short enough that a tag
|
|
40
|
+
* for an agent that never really started clears quickly.
|
|
41
|
+
*/
|
|
42
|
+
const LAUNCH_GRACE_MS = 4000;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Source-of-truth model for the running-terminals panel. Each running
|
|
46
|
+
* terminal session known to the server is one entry; the registry caches
|
|
47
|
+
* the last `widget.title.label` we observed so the agent name (or any
|
|
48
|
+
* xterm-published title) survives the user closing the tab while the
|
|
49
|
+
* session continues running on the backend.
|
|
50
|
+
*
|
|
51
|
+
* It also resolves *which agent is running* in each session, so the panel can
|
|
52
|
+
* badge rows with the agent's logo. Two inputs feed that, reconciled by
|
|
53
|
+
* {@link agentCommandFor}:
|
|
54
|
+
* - an optimistic launch tag ({@link IAgentSessions}) written when we start
|
|
55
|
+
* an agent ourselves — instant, but blind to the agent later exiting; and
|
|
56
|
+
* - authoritative server-side process detection, polled here, which works
|
|
57
|
+
* for hand-started agents too and clears once an agent exits.
|
|
58
|
+
*
|
|
59
|
+
* Upstream session signals:
|
|
60
|
+
* - `serviceManager.terminals.runningChanged` is the authoritative
|
|
61
|
+
* list of live sessions; everything not in it has been shut down
|
|
62
|
+
* server-side and we must drop it.
|
|
63
|
+
* - `tracker.widgetAdded` plus the per-widget `title.changed` /
|
|
64
|
+
* `disposed` signals keep our label cache in sync with whatever
|
|
65
|
+
* xterm/the launcher has set on the open tabs.
|
|
66
|
+
*
|
|
67
|
+
* `stateChanged` is emitted for every kind of update; the panel hooks it
|
|
68
|
+
* through a `UseSignal` so a single subscription re-renders the list on
|
|
69
|
+
* any change.
|
|
70
|
+
*/
|
|
71
|
+
export class SessionRegistry implements IDisposable {
|
|
72
|
+
constructor(options: SessionRegistry.IOptions) {
|
|
73
|
+
this._terminals = options.serviceManager.terminals;
|
|
74
|
+
this._tracker = options.tracker;
|
|
75
|
+
this._agentSessions = options.agentSessions ?? null;
|
|
76
|
+
this._detectCommands = options.detectCommands ?? (() => []);
|
|
77
|
+
this._shell = options.shell ?? null;
|
|
78
|
+
|
|
79
|
+
this._terminals.runningChanged.connect(this._onRunningChanged, this);
|
|
80
|
+
this._tracker.widgetAdded.connect(this._onWidgetAdded, this);
|
|
81
|
+
this._tracker.forEach(widget => this._trackWidget(widget));
|
|
82
|
+
this._refreshLive();
|
|
83
|
+
|
|
84
|
+
// Track which terminal is the current widget in the main area so the panel
|
|
85
|
+
// can highlight its row — and highlight nothing while a notebook or any
|
|
86
|
+
// other non-terminal tab is current instead. `currentChanged` is optional
|
|
87
|
+
// on the shell interface (not every shell can switch focus), so guard it;
|
|
88
|
+
// when it is absent the highlight stays off.
|
|
89
|
+
this._shell?.currentChanged?.connect(this._onShellCurrentChanged, this);
|
|
90
|
+
this._updateCurrent();
|
|
91
|
+
|
|
92
|
+
// A freshly written launch tag should re-render the panel immediately
|
|
93
|
+
// (show the logo) and again once the grace window closes (so a tag for
|
|
94
|
+
// an agent that never started, or that detection later contradicts,
|
|
95
|
+
// doesn't linger).
|
|
96
|
+
this._agentSessions?.changed.connect(this._onTagChanged, this);
|
|
97
|
+
|
|
98
|
+
this._poll = new Poll({
|
|
99
|
+
name: '@xtralab/terminals:runningAgents',
|
|
100
|
+
factory: () => this._refreshDetection(),
|
|
101
|
+
frequency: {
|
|
102
|
+
interval: DETECT_POLL_INTERVAL_MS,
|
|
103
|
+
backoff: true,
|
|
104
|
+
max: DETECT_POLL_MAX_MS
|
|
105
|
+
},
|
|
106
|
+
standby: 'when-hidden'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Emitted whenever the live session list, a cached label, or the detected
|
|
112
|
+
* running agent changes.
|
|
113
|
+
*/
|
|
114
|
+
get stateChanged(): ISignal<this, void> {
|
|
115
|
+
return this._stateChanged;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get isDisposed(): boolean {
|
|
119
|
+
return this._isDisposed;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Names of all live sessions, ordered by their stable rank so the
|
|
124
|
+
* rendered list keeps a consistent order (roughly creation order) as
|
|
125
|
+
* new sessions are added and old ones shut down.
|
|
126
|
+
*/
|
|
127
|
+
sessionNames(): string[] {
|
|
128
|
+
const names = Array.from(this._live);
|
|
129
|
+
names.sort((a, b) => this.rankFor(a) - this.rankFor(b));
|
|
130
|
+
return names;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True iff the named session is still on the server. Used by the panel
|
|
135
|
+
* to skip a row during the brief window between the session's shutdown
|
|
136
|
+
* and the next `runningChanged` arriving.
|
|
137
|
+
*/
|
|
138
|
+
has(name: string): boolean {
|
|
139
|
+
return this._live.has(name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Display name for the session. The cache wins because it only
|
|
144
|
+
* holds "real" labels (the launcher's agent name or an xterm escape
|
|
145
|
+
* sequence — see `_cacheLabel` for the filter); reading it first
|
|
146
|
+
* prevents the transient `Terminal {name}` an XTerm widget shows
|
|
147
|
+
* during reconnect from flickering into the panel. The live widget
|
|
148
|
+
* title is used when no cache exists, and the `Terminal {name}`
|
|
149
|
+
* fallback covers sessions we have never seen a widget for (e.g.
|
|
150
|
+
* surviving a lab reload).
|
|
151
|
+
*/
|
|
152
|
+
labelFor(name: string): string {
|
|
153
|
+
const cached = this._labels.get(name);
|
|
154
|
+
if (cached) {
|
|
155
|
+
return cached;
|
|
156
|
+
}
|
|
157
|
+
const widget = this.widgetFor(name);
|
|
158
|
+
if (widget && widget.title.label) {
|
|
159
|
+
return widget.title.label;
|
|
160
|
+
}
|
|
161
|
+
return `Terminal ${name}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The command of the agent running in the session, or `null` if none.
|
|
166
|
+
*
|
|
167
|
+
* Server-side detection is authoritative whenever it reports a running
|
|
168
|
+
* agent. A launch tag fills two gaps: the startup grace window right after
|
|
169
|
+
* we launch an agent (so its logo shows before its process is detectable),
|
|
170
|
+
* and any time detection is unavailable (older server, transient error).
|
|
171
|
+
* Once the grace window has passed and detection has reported the session
|
|
172
|
+
* as idle, the tag is ignored — that is how the badge clears when an agent
|
|
173
|
+
* exits.
|
|
174
|
+
*/
|
|
175
|
+
agentCommandFor(name: string): string | null {
|
|
176
|
+
// `string` = detected running agent, `null` = polled and idle,
|
|
177
|
+
// `undefined` = not covered by a successful poll yet.
|
|
178
|
+
const detected = this._detected.has(name)
|
|
179
|
+
? this._detected.get(name)!
|
|
180
|
+
: undefined;
|
|
181
|
+
if (typeof detected === 'string') {
|
|
182
|
+
return detected;
|
|
183
|
+
}
|
|
184
|
+
const tagged = this._agentSessions?.get(name) ?? null;
|
|
185
|
+
if (tagged !== null) {
|
|
186
|
+
const seenAt = this._firstSeen.get(name) ?? 0;
|
|
187
|
+
const inGrace = Date.now() - seenAt < LAUNCH_GRACE_MS;
|
|
188
|
+
if (inGrace || detected === undefined) {
|
|
189
|
+
return tagged;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Return the open widget for a session, if any. The panel uses this to
|
|
197
|
+
* switch behaviour between "activate existing tab" and "open a new tab
|
|
198
|
+
* connected to the live session".
|
|
199
|
+
*/
|
|
200
|
+
widgetFor(name: string): TerminalWidget | null {
|
|
201
|
+
return (
|
|
202
|
+
this._tracker.find(widget => widget.content.session.name === name) ?? null
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Session name of the terminal that is currently the active widget in the
|
|
208
|
+
* shell's main area, or `null` when that widget is not a terminal (for
|
|
209
|
+
* example a notebook or text editor is current). The panel uses this to
|
|
210
|
+
* highlight the current terminal's row — mirroring how the file browser
|
|
211
|
+
* surfaces the open document — and to leave every row unhighlighted while a
|
|
212
|
+
* non-terminal tab is current.
|
|
213
|
+
*/
|
|
214
|
+
currentSessionName(): string | null {
|
|
215
|
+
return this._currentName;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Stable per-session rank assigned in observation order. Used to keep
|
|
220
|
+
* the rendered list in a steady order so rows don't reshuffle as
|
|
221
|
+
* sessions come and go. Cleaned up when the session is shut down so the
|
|
222
|
+
* counter does not grow without bound across long sessions.
|
|
223
|
+
*/
|
|
224
|
+
rankFor(name: string): number {
|
|
225
|
+
let rank = this._ranks.get(name);
|
|
226
|
+
if (rank === undefined) {
|
|
227
|
+
rank = this._nextRank++;
|
|
228
|
+
this._ranks.set(name, rank);
|
|
229
|
+
}
|
|
230
|
+
return rank;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
dispose(): void {
|
|
234
|
+
if (this._isDisposed) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this._isDisposed = true;
|
|
238
|
+
this._poll.dispose();
|
|
239
|
+
this._terminals.runningChanged.disconnect(this._onRunningChanged, this);
|
|
240
|
+
this._tracker.widgetAdded.disconnect(this._onWidgetAdded, this);
|
|
241
|
+
this._agentSessions?.changed.disconnect(this._onTagChanged, this);
|
|
242
|
+
this._shell?.currentChanged?.disconnect(this._onShellCurrentChanged, this);
|
|
243
|
+
this._tracker.forEach(widget => this._untrackWidget(widget));
|
|
244
|
+
Signal.clearData(this);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private _refreshLive(): void {
|
|
248
|
+
const next = new Set<string>();
|
|
249
|
+
for (const model of this._terminals.running()) {
|
|
250
|
+
next.add(model.name);
|
|
251
|
+
}
|
|
252
|
+
this._reconcileLive(next);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private _onRunningChanged(_: unknown, sessions: Terminal.IModel[]): void {
|
|
256
|
+
this._reconcileLive(new Set(sessions.map(model => model.name)));
|
|
257
|
+
this._stateChanged.emit();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Adopt `next` as the live set and prune every per-session map for
|
|
262
|
+
* sessions that have gone away — labels, ranks, first-seen timestamps,
|
|
263
|
+
* detection results, and the shared launch tag. Without this the maps (and
|
|
264
|
+
* the rank counter) would grow without bound as terminals come and go.
|
|
265
|
+
*/
|
|
266
|
+
private _reconcileLive(next: Set<string>): void {
|
|
267
|
+
// Names we had already seen, captured before we prune below — the set of
|
|
268
|
+
// candidates whose launch tag may now need forgetting. (The tag map is
|
|
269
|
+
// not enumerable, so we drive the prune from the sessions we know about;
|
|
270
|
+
// `_firstSeen` has an entry for every session that has ever been live.)
|
|
271
|
+
const known = Array.from(this._firstSeen.keys());
|
|
272
|
+
|
|
273
|
+
for (const name of next) {
|
|
274
|
+
if (!this._firstSeen.has(name)) {
|
|
275
|
+
this._firstSeen.set(name, Date.now());
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this._live = next;
|
|
279
|
+
for (const name of Array.from(this._labels.keys())) {
|
|
280
|
+
if (!next.has(name)) {
|
|
281
|
+
this._labels.delete(name);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const name of Array.from(this._ranks.keys())) {
|
|
285
|
+
if (!next.has(name)) {
|
|
286
|
+
this._ranks.delete(name);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const name of Array.from(this._firstSeen.keys())) {
|
|
290
|
+
if (!next.has(name)) {
|
|
291
|
+
this._firstSeen.delete(name);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const name of Array.from(this._detected.keys())) {
|
|
295
|
+
if (!next.has(name)) {
|
|
296
|
+
this._detected.delete(name);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Forget launch tags for sessions that have gone away. This matters for
|
|
300
|
+
// correctness as well as bookkeeping: terminado reuses session names, so
|
|
301
|
+
// a stale tag could otherwise mislabel a brand-new terminal that happens
|
|
302
|
+
// to reuse a closed session's name.
|
|
303
|
+
for (const name of known) {
|
|
304
|
+
if (!next.has(name)) {
|
|
305
|
+
this._agentSessions?.delete(name);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Poll body: ask the server which agent runs in each terminal and update
|
|
312
|
+
* the detection map. On failure we keep the previous results rather than
|
|
313
|
+
* clearing them, so a transient error doesn't strip every badge.
|
|
314
|
+
*/
|
|
315
|
+
private async _refreshDetection(): Promise<void> {
|
|
316
|
+
const commands = this._detectCommands();
|
|
317
|
+
if (commands.length === 0) {
|
|
318
|
+
if (this._detected.size > 0) {
|
|
319
|
+
this._detected = new Map();
|
|
320
|
+
this._stateChanged.emit();
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const result = await fetchRunningAgents(commands);
|
|
325
|
+
if (result === null) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const next = new Map<string, string | null>();
|
|
329
|
+
for (const [name, command] of Object.entries(result)) {
|
|
330
|
+
if (this._live.has(name)) {
|
|
331
|
+
next.set(name, command);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!Private.detectedEqual(this._detected, next)) {
|
|
335
|
+
this._detected = next;
|
|
336
|
+
this._stateChanged.emit();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private _onTagChanged(): void {
|
|
341
|
+
this._stateChanged.emit();
|
|
342
|
+
// Re-render once the grace window closes so a tag that detection never
|
|
343
|
+
// confirmed (or has since contradicted) stops being shown.
|
|
344
|
+
setTimeout(() => {
|
|
345
|
+
if (!this._isDisposed) {
|
|
346
|
+
this._stateChanged.emit();
|
|
347
|
+
}
|
|
348
|
+
}, LAUNCH_GRACE_MS + 100);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private _onWidgetAdded(_: unknown, widget: TerminalWidget): void {
|
|
352
|
+
this._trackWidget(widget);
|
|
353
|
+
this._stateChanged.emit();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private _onShellCurrentChanged(): void {
|
|
357
|
+
this._updateCurrent();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Recompute which terminal (if any) is the active main-area widget and emit
|
|
362
|
+
* only when it changes. The terminal tracker tells us whether the shell's
|
|
363
|
+
* current widget is one of our terminals; anything else — a notebook, an
|
|
364
|
+
* editor, or nothing — clears the highlight.
|
|
365
|
+
*/
|
|
366
|
+
private _updateCurrent(): void {
|
|
367
|
+
const current = this._shell?.currentWidget ?? null;
|
|
368
|
+
const name =
|
|
369
|
+
current && this._tracker.has(current)
|
|
370
|
+
? (current as TerminalWidget).content.session.name
|
|
371
|
+
: null;
|
|
372
|
+
if (name !== this._currentName) {
|
|
373
|
+
this._currentName = name;
|
|
374
|
+
this._stateChanged.emit();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private _trackWidget(widget: TerminalWidget): void {
|
|
379
|
+
const name = widget.content.session.name;
|
|
380
|
+
this._cacheLabel(name, widget.title.label);
|
|
381
|
+
widget.title.changed.connect(this._onTitleChanged, this);
|
|
382
|
+
widget.disposed.connect(this._onWidgetDisposed, this);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private _untrackWidget(widget: TerminalWidget): void {
|
|
386
|
+
widget.title.changed.disconnect(this._onTitleChanged, this);
|
|
387
|
+
widget.disposed.disconnect(this._onWidgetDisposed, this);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private _onTitleChanged(title: Title<Widget>): void {
|
|
391
|
+
const owner = title.owner as TerminalWidget;
|
|
392
|
+
const session = owner.content?.session;
|
|
393
|
+
if (!session) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this._cacheLabel(session.name, title.label);
|
|
397
|
+
this._stateChanged.emit();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Update the cached label for a session, but only when the new
|
|
402
|
+
* label is "real" — non-empty and not one of the transient defaults
|
|
403
|
+
* the XTerm widget cycles through during reconnect (`'...'` while
|
|
404
|
+
* the websocket is opening, `'Terminal {name}'` once
|
|
405
|
+
* `_initialConnection` fires). Without the filter, briefly
|
|
406
|
+
* reopening a tab while an agent has yet to re-emit its xterm
|
|
407
|
+
* title escape sequence would clobber the cached agent name with
|
|
408
|
+
* `Terminal 1`, which is exactly the regression we are guarding
|
|
409
|
+
* against. Live widget titles still display whatever the widget
|
|
410
|
+
* currently holds because `labelFor` consults the widget first;
|
|
411
|
+
* the filter only affects what survives a tab close.
|
|
412
|
+
*/
|
|
413
|
+
private _cacheLabel(name: string, label: string): void {
|
|
414
|
+
if (!label) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (label === '...' || label === `Terminal ${name}`) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
this._labels.set(name, label);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private _onWidgetDisposed(widget: Widget): void {
|
|
424
|
+
// The widget is gone but the session may still be running on the
|
|
425
|
+
// server — keep the cached label so the panel keeps the agent's name
|
|
426
|
+
// when the user reopens the tab. We only clean up our subscriptions
|
|
427
|
+
// here; the cache is purged by `runningChanged` once the session
|
|
428
|
+
// itself goes away.
|
|
429
|
+
this._untrackWidget(widget as TerminalWidget);
|
|
430
|
+
this._stateChanged.emit();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private _terminals: Terminal.IManager;
|
|
434
|
+
private _tracker: ITerminalTracker;
|
|
435
|
+
private _agentSessions: IAgentSessions | null;
|
|
436
|
+
private _detectCommands: () => string[];
|
|
437
|
+
private _shell: JupyterFrontEnd.IShell | null;
|
|
438
|
+
private _poll: Poll;
|
|
439
|
+
private _labels = new Map<string, string>();
|
|
440
|
+
private _ranks = new Map<string, number>();
|
|
441
|
+
private _firstSeen = new Map<string, number>();
|
|
442
|
+
private _detected = new Map<string, string | null>();
|
|
443
|
+
private _live = new Set<string>();
|
|
444
|
+
private _currentName: string | null = null;
|
|
445
|
+
private _nextRank = 100;
|
|
446
|
+
private _isDisposed = false;
|
|
447
|
+
private _stateChanged = new Signal<this, void>(this);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Construction options for {@link SessionRegistry}.
|
|
452
|
+
*/
|
|
453
|
+
export namespace SessionRegistry {
|
|
454
|
+
export interface IOptions {
|
|
455
|
+
serviceManager: ServiceManager.IManager;
|
|
456
|
+
tracker: ITerminalTracker;
|
|
457
|
+
/**
|
|
458
|
+
* The application shell, used to tell which widget is currently active so
|
|
459
|
+
* the panel can highlight the terminal that is the current main-area
|
|
460
|
+
* widget — and highlight nothing when that widget is not a terminal (a
|
|
461
|
+
* notebook, an editor, …). Optional: without it, or on a shell that cannot
|
|
462
|
+
* report `currentChanged`, the highlight stays off.
|
|
463
|
+
*/
|
|
464
|
+
shell?: JupyterFrontEnd.IShell | null;
|
|
465
|
+
/**
|
|
466
|
+
* Shared launch-tag registry. When present, its records seed each row's
|
|
467
|
+
* agent badge until server-side detection takes over.
|
|
468
|
+
*/
|
|
469
|
+
agentSessions?: IAgentSessions | null;
|
|
470
|
+
/**
|
|
471
|
+
* Returns the agent commands the server should look for when detecting
|
|
472
|
+
* running agents. Read on every poll so it tracks the live agent list.
|
|
473
|
+
*/
|
|
474
|
+
detectCommands?: () => string[];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
namespace Private {
|
|
479
|
+
/**
|
|
480
|
+
* Value-equality for two detection maps, so a poll that changes nothing
|
|
481
|
+
* doesn't trigger a re-render.
|
|
482
|
+
*/
|
|
483
|
+
export function detectedEqual(
|
|
484
|
+
a: Map<string, string | null>,
|
|
485
|
+
b: Map<string, string | null>
|
|
486
|
+
): boolean {
|
|
487
|
+
if (a.size !== b.size) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
for (const [name, command] of a) {
|
|
491
|
+
if (!b.has(name) || b.get(name) !== command) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
}
|