xtralab 0.4.0 → 0.6.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.
Files changed (96) hide show
  1. package/README.md +110 -73
  2. package/lib/agentSessions.d.ts +50 -0
  3. package/lib/agentSessions.js +45 -0
  4. package/lib/fileBrowser/index.js +5 -1
  5. package/lib/git/commands.d.ts +12 -13
  6. package/lib/git/commands.js +7 -28
  7. package/lib/git/diffProvider.d.ts +8 -0
  8. package/lib/git/diffProvider.js +314 -0
  9. package/lib/git/diffSurface.d.ts +115 -0
  10. package/lib/git/diffSurface.js +342 -0
  11. package/lib/git/diffWidget.d.ts +8 -13
  12. package/lib/git/diffWidget.js +65 -311
  13. package/lib/git/imageDiff.d.ts +23 -0
  14. package/lib/git/imageDiff.js +256 -0
  15. package/lib/git/index.d.ts +10 -8
  16. package/lib/git/index.js +37 -52
  17. package/lib/index.d.ts +4 -2
  18. package/lib/index.js +14 -8
  19. package/lib/launcher/agents.d.ts +1 -1
  20. package/lib/launcher/agents.js +22 -13
  21. package/lib/launcher/commands.d.ts +32 -6
  22. package/lib/launcher/commands.js +81 -57
  23. package/lib/launcher/dashboard.d.ts +15 -0
  24. package/lib/launcher/dashboard.js +119 -5
  25. package/lib/launcher/editorRegistry.d.ts +43 -0
  26. package/lib/launcher/editorRegistry.js +96 -0
  27. package/lib/launcher/editors.d.ts +78 -0
  28. package/lib/launcher/editors.js +110 -0
  29. package/lib/launcher/icons.d.ts +25 -6
  30. package/lib/launcher/icons.js +116 -19
  31. package/lib/launcher/index.d.ts +2 -22
  32. package/lib/launcher/index.js +48 -25
  33. package/lib/launcher/registry.d.ts +21 -0
  34. package/lib/launcher/registry.js +28 -0
  35. package/lib/launcher/tokens.d.ts +41 -0
  36. package/lib/launcher/tokens.js +17 -0
  37. package/lib/terminals/detection.d.ts +11 -0
  38. package/lib/terminals/detection.js +50 -0
  39. package/lib/terminals/index.d.ts +27 -0
  40. package/lib/terminals/index.js +194 -0
  41. package/lib/terminals/model.d.ts +191 -0
  42. package/lib/terminals/model.js +418 -0
  43. package/lib/terminals/widget.d.ts +72 -0
  44. package/lib/terminals/widget.js +93 -0
  45. package/lib/topBar/icons.d.ts +13 -0
  46. package/lib/topBar/icons.js +27 -0
  47. package/lib/topBar/index.d.ts +16 -0
  48. package/lib/topBar/index.js +86 -0
  49. package/package.json +52 -35
  50. package/schema/launcher.json +56 -1
  51. package/src/agentSessions.ts +94 -0
  52. package/src/fileBrowser/index.ts +5 -1
  53. package/src/git/commands.ts +12 -38
  54. package/src/git/diffProvider.tsx +462 -0
  55. package/src/git/diffSurface.tsx +588 -0
  56. package/src/git/diffWidget.tsx +105 -465
  57. package/src/git/imageDiff.tsx +469 -0
  58. package/src/git/index.ts +38 -68
  59. package/src/index.ts +14 -8
  60. package/src/launcher/agents.ts +23 -14
  61. package/src/launcher/commands.ts +98 -60
  62. package/src/launcher/dashboard.tsx +205 -3
  63. package/src/launcher/editorRegistry.ts +149 -0
  64. package/src/launcher/editors.ts +183 -0
  65. package/src/launcher/icons.ts +120 -20
  66. package/src/launcher/index.ts +62 -27
  67. package/src/launcher/registry.ts +33 -0
  68. package/src/launcher/tokens.ts +51 -0
  69. package/src/terminals/detection.ts +65 -0
  70. package/src/terminals/index.ts +222 -0
  71. package/src/terminals/model.ts +497 -0
  72. package/src/terminals/widget.tsx +240 -0
  73. package/src/topBar/icons.ts +30 -0
  74. package/src/topBar/index.ts +121 -0
  75. package/style/git.css +267 -266
  76. package/style/index.css +2 -1
  77. package/style/index.js +2 -1
  78. package/style/launcher.css +124 -2
  79. package/style/terminals.css +235 -0
  80. package/style/topBar.css +38 -0
  81. package/lib/git/icons.d.ts +0 -14
  82. package/lib/git/icons.js +0 -19
  83. package/lib/git/panel.d.ts +0 -29
  84. package/lib/git/panel.js +0 -42
  85. package/lib/git/panelComponent.d.ts +0 -28
  86. package/lib/git/panelComponent.js +0 -320
  87. package/lib/statusBar/index.d.ts +0 -14
  88. package/lib/statusBar/index.js +0 -89
  89. package/lib/statusBar/widget.d.ts +0 -121
  90. package/lib/statusBar/widget.js +0 -237
  91. package/src/git/icons.ts +0 -20
  92. package/src/git/panel.tsx +0 -56
  93. package/src/git/panelComponent.tsx +0 -575
  94. package/src/statusBar/index.ts +0 -115
  95. package/src/statusBar/widget.tsx +0 -318
  96. package/style/statusBar.css +0 -95
@@ -0,0 +1,418 @@
1
+ import { Poll } from '@lumino/polling';
2
+ import { Signal } from '@lumino/signaling';
3
+ import { fetchRunningAgents } from './detection';
4
+ /**
5
+ * How often to ask the server which agent (if any) is running in each
6
+ * terminal. Snappy enough that a manually-started agent's logo shows up
7
+ * within a few seconds, light enough that walking the shells' process trees
8
+ * stays negligible.
9
+ */
10
+ const DETECT_POLL_INTERVAL_MS = 3000;
11
+ /**
12
+ * Upper bound on the exponential backoff when detection fails repeatedly
13
+ * (e.g. the endpoint is missing on an older server). Matches the rest of the
14
+ * plugin's polls.
15
+ */
16
+ const DETECT_POLL_MAX_MS = 300000;
17
+ /**
18
+ * Grace window after a session first appears during which an optimistic
19
+ * launch tag outranks a `null` detection result. It covers the gap between
20
+ * issuing an agent's command and its process actually being spawnable, so the
21
+ * freshly-launched logo doesn't blink off if a detection poll lands in that
22
+ * sliver. Comfortably longer than a cold agent start; short enough that a tag
23
+ * for an agent that never really started clears quickly.
24
+ */
25
+ const LAUNCH_GRACE_MS = 4000;
26
+ /**
27
+ * Source-of-truth model for the running-terminals panel. Each running
28
+ * terminal session known to the server is one entry; the registry caches
29
+ * the last `widget.title.label` we observed so the agent name (or any
30
+ * xterm-published title) survives the user closing the tab while the
31
+ * session continues running on the backend.
32
+ *
33
+ * It also resolves *which agent is running* in each session, so the panel can
34
+ * badge rows with the agent's logo. Two inputs feed that, reconciled by
35
+ * {@link agentCommandFor}:
36
+ * - an optimistic launch tag ({@link IAgentSessions}) written when we start
37
+ * an agent ourselves — instant, but blind to the agent later exiting; and
38
+ * - authoritative server-side process detection, polled here, which works
39
+ * for hand-started agents too and clears once an agent exits.
40
+ *
41
+ * Upstream session signals:
42
+ * - `serviceManager.terminals.runningChanged` is the authoritative
43
+ * list of live sessions; everything not in it has been shut down
44
+ * server-side and we must drop it.
45
+ * - `tracker.widgetAdded` plus the per-widget `title.changed` /
46
+ * `disposed` signals keep our label cache in sync with whatever
47
+ * xterm/the launcher has set on the open tabs.
48
+ *
49
+ * `stateChanged` is emitted for every kind of update; the panel hooks it
50
+ * through a `UseSignal` so a single subscription re-renders the list on
51
+ * any change.
52
+ */
53
+ export class SessionRegistry {
54
+ constructor(options) {
55
+ var _a, _b, _c, _d, _e, _f;
56
+ this._labels = new Map();
57
+ this._ranks = new Map();
58
+ this._firstSeen = new Map();
59
+ this._detected = new Map();
60
+ this._live = new Set();
61
+ this._currentName = null;
62
+ this._nextRank = 100;
63
+ this._isDisposed = false;
64
+ this._stateChanged = new Signal(this);
65
+ this._terminals = options.serviceManager.terminals;
66
+ this._tracker = options.tracker;
67
+ this._agentSessions = (_a = options.agentSessions) !== null && _a !== void 0 ? _a : null;
68
+ this._detectCommands = (_b = options.detectCommands) !== null && _b !== void 0 ? _b : (() => []);
69
+ this._shell = (_c = options.shell) !== null && _c !== void 0 ? _c : null;
70
+ this._terminals.runningChanged.connect(this._onRunningChanged, this);
71
+ this._tracker.widgetAdded.connect(this._onWidgetAdded, this);
72
+ this._tracker.forEach(widget => this._trackWidget(widget));
73
+ this._refreshLive();
74
+ // Track which terminal is the current widget in the main area so the panel
75
+ // can highlight its row — and highlight nothing while a notebook or any
76
+ // other non-terminal tab is current instead. `currentChanged` is optional
77
+ // on the shell interface (not every shell can switch focus), so guard it;
78
+ // when it is absent the highlight stays off.
79
+ (_e = (_d = this._shell) === null || _d === void 0 ? void 0 : _d.currentChanged) === null || _e === void 0 ? void 0 : _e.connect(this._onShellCurrentChanged, this);
80
+ this._updateCurrent();
81
+ // A freshly written launch tag should re-render the panel immediately
82
+ // (show the logo) and again once the grace window closes (so a tag for
83
+ // an agent that never started, or that detection later contradicts,
84
+ // doesn't linger).
85
+ (_f = this._agentSessions) === null || _f === void 0 ? void 0 : _f.changed.connect(this._onTagChanged, this);
86
+ this._poll = new Poll({
87
+ name: '@xtralab/terminals:runningAgents',
88
+ factory: () => this._refreshDetection(),
89
+ frequency: {
90
+ interval: DETECT_POLL_INTERVAL_MS,
91
+ backoff: true,
92
+ max: DETECT_POLL_MAX_MS
93
+ },
94
+ standby: 'when-hidden'
95
+ });
96
+ }
97
+ /**
98
+ * Emitted whenever the live session list, a cached label, or the detected
99
+ * running agent changes.
100
+ */
101
+ get stateChanged() {
102
+ return this._stateChanged;
103
+ }
104
+ get isDisposed() {
105
+ return this._isDisposed;
106
+ }
107
+ /**
108
+ * Names of all live sessions, ordered by their stable rank so the
109
+ * rendered list keeps a consistent order (roughly creation order) as
110
+ * new sessions are added and old ones shut down.
111
+ */
112
+ sessionNames() {
113
+ const names = Array.from(this._live);
114
+ names.sort((a, b) => this.rankFor(a) - this.rankFor(b));
115
+ return names;
116
+ }
117
+ /**
118
+ * True iff the named session is still on the server. Used by the panel
119
+ * to skip a row during the brief window between the session's shutdown
120
+ * and the next `runningChanged` arriving.
121
+ */
122
+ has(name) {
123
+ return this._live.has(name);
124
+ }
125
+ /**
126
+ * Display name for the session. The cache wins because it only
127
+ * holds "real" labels (the launcher's agent name or an xterm escape
128
+ * sequence — see `_cacheLabel` for the filter); reading it first
129
+ * prevents the transient `Terminal {name}` an XTerm widget shows
130
+ * during reconnect from flickering into the panel. The live widget
131
+ * title is used when no cache exists, and the `Terminal {name}`
132
+ * fallback covers sessions we have never seen a widget for (e.g.
133
+ * surviving a lab reload).
134
+ */
135
+ labelFor(name) {
136
+ const cached = this._labels.get(name);
137
+ if (cached) {
138
+ return cached;
139
+ }
140
+ const widget = this.widgetFor(name);
141
+ if (widget && widget.title.label) {
142
+ return widget.title.label;
143
+ }
144
+ return `Terminal ${name}`;
145
+ }
146
+ /**
147
+ * The command of the agent running in the session, or `null` if none.
148
+ *
149
+ * Server-side detection is authoritative whenever it reports a running
150
+ * agent. A launch tag fills two gaps: the startup grace window right after
151
+ * we launch an agent (so its logo shows before its process is detectable),
152
+ * and any time detection is unavailable (older server, transient error).
153
+ * Once the grace window has passed and detection has reported the session
154
+ * as idle, the tag is ignored — that is how the badge clears when an agent
155
+ * exits.
156
+ */
157
+ agentCommandFor(name) {
158
+ var _a, _b, _c;
159
+ // `string` = detected running agent, `null` = polled and idle,
160
+ // `undefined` = not covered by a successful poll yet.
161
+ const detected = this._detected.has(name)
162
+ ? this._detected.get(name)
163
+ : undefined;
164
+ if (typeof detected === 'string') {
165
+ return detected;
166
+ }
167
+ const tagged = (_b = (_a = this._agentSessions) === null || _a === void 0 ? void 0 : _a.get(name)) !== null && _b !== void 0 ? _b : null;
168
+ if (tagged !== null) {
169
+ const seenAt = (_c = this._firstSeen.get(name)) !== null && _c !== void 0 ? _c : 0;
170
+ const inGrace = Date.now() - seenAt < LAUNCH_GRACE_MS;
171
+ if (inGrace || detected === undefined) {
172
+ return tagged;
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+ /**
178
+ * Return the open widget for a session, if any. The panel uses this to
179
+ * switch behaviour between "activate existing tab" and "open a new tab
180
+ * connected to the live session".
181
+ */
182
+ widgetFor(name) {
183
+ var _a;
184
+ return ((_a = this._tracker.find(widget => widget.content.session.name === name)) !== null && _a !== void 0 ? _a : null);
185
+ }
186
+ /**
187
+ * Session name of the terminal that is currently the active widget in the
188
+ * shell's main area, or `null` when that widget is not a terminal (for
189
+ * example a notebook or text editor is current). The panel uses this to
190
+ * highlight the current terminal's row — mirroring how the file browser
191
+ * surfaces the open document — and to leave every row unhighlighted while a
192
+ * non-terminal tab is current.
193
+ */
194
+ currentSessionName() {
195
+ return this._currentName;
196
+ }
197
+ /**
198
+ * Stable per-session rank assigned in observation order. Used to keep
199
+ * the rendered list in a steady order so rows don't reshuffle as
200
+ * sessions come and go. Cleaned up when the session is shut down so the
201
+ * counter does not grow without bound across long sessions.
202
+ */
203
+ rankFor(name) {
204
+ let rank = this._ranks.get(name);
205
+ if (rank === undefined) {
206
+ rank = this._nextRank++;
207
+ this._ranks.set(name, rank);
208
+ }
209
+ return rank;
210
+ }
211
+ dispose() {
212
+ var _a, _b, _c;
213
+ if (this._isDisposed) {
214
+ return;
215
+ }
216
+ this._isDisposed = true;
217
+ this._poll.dispose();
218
+ this._terminals.runningChanged.disconnect(this._onRunningChanged, this);
219
+ this._tracker.widgetAdded.disconnect(this._onWidgetAdded, this);
220
+ (_a = this._agentSessions) === null || _a === void 0 ? void 0 : _a.changed.disconnect(this._onTagChanged, this);
221
+ (_c = (_b = this._shell) === null || _b === void 0 ? void 0 : _b.currentChanged) === null || _c === void 0 ? void 0 : _c.disconnect(this._onShellCurrentChanged, this);
222
+ this._tracker.forEach(widget => this._untrackWidget(widget));
223
+ Signal.clearData(this);
224
+ }
225
+ _refreshLive() {
226
+ const next = new Set();
227
+ for (const model of this._terminals.running()) {
228
+ next.add(model.name);
229
+ }
230
+ this._reconcileLive(next);
231
+ }
232
+ _onRunningChanged(_, sessions) {
233
+ this._reconcileLive(new Set(sessions.map(model => model.name)));
234
+ this._stateChanged.emit();
235
+ }
236
+ /**
237
+ * Adopt `next` as the live set and prune every per-session map for
238
+ * sessions that have gone away — labels, ranks, first-seen timestamps,
239
+ * detection results, and the shared launch tag. Without this the maps (and
240
+ * the rank counter) would grow without bound as terminals come and go.
241
+ */
242
+ _reconcileLive(next) {
243
+ var _a;
244
+ // Names we had already seen, captured before we prune below — the set of
245
+ // candidates whose launch tag may now need forgetting. (The tag map is
246
+ // not enumerable, so we drive the prune from the sessions we know about;
247
+ // `_firstSeen` has an entry for every session that has ever been live.)
248
+ const known = Array.from(this._firstSeen.keys());
249
+ for (const name of next) {
250
+ if (!this._firstSeen.has(name)) {
251
+ this._firstSeen.set(name, Date.now());
252
+ }
253
+ }
254
+ this._live = next;
255
+ for (const name of Array.from(this._labels.keys())) {
256
+ if (!next.has(name)) {
257
+ this._labels.delete(name);
258
+ }
259
+ }
260
+ for (const name of Array.from(this._ranks.keys())) {
261
+ if (!next.has(name)) {
262
+ this._ranks.delete(name);
263
+ }
264
+ }
265
+ for (const name of Array.from(this._firstSeen.keys())) {
266
+ if (!next.has(name)) {
267
+ this._firstSeen.delete(name);
268
+ }
269
+ }
270
+ for (const name of Array.from(this._detected.keys())) {
271
+ if (!next.has(name)) {
272
+ this._detected.delete(name);
273
+ }
274
+ }
275
+ // Forget launch tags for sessions that have gone away. This matters for
276
+ // correctness as well as bookkeeping: terminado reuses session names, so
277
+ // a stale tag could otherwise mislabel a brand-new terminal that happens
278
+ // to reuse a closed session's name.
279
+ for (const name of known) {
280
+ if (!next.has(name)) {
281
+ (_a = this._agentSessions) === null || _a === void 0 ? void 0 : _a.delete(name);
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Poll body: ask the server which agent runs in each terminal and update
287
+ * the detection map. On failure we keep the previous results rather than
288
+ * clearing them, so a transient error doesn't strip every badge.
289
+ */
290
+ async _refreshDetection() {
291
+ const commands = this._detectCommands();
292
+ if (commands.length === 0) {
293
+ if (this._detected.size > 0) {
294
+ this._detected = new Map();
295
+ this._stateChanged.emit();
296
+ }
297
+ return;
298
+ }
299
+ const result = await fetchRunningAgents(commands);
300
+ if (result === null) {
301
+ return;
302
+ }
303
+ const next = new Map();
304
+ for (const [name, command] of Object.entries(result)) {
305
+ if (this._live.has(name)) {
306
+ next.set(name, command);
307
+ }
308
+ }
309
+ if (!Private.detectedEqual(this._detected, next)) {
310
+ this._detected = next;
311
+ this._stateChanged.emit();
312
+ }
313
+ }
314
+ _onTagChanged() {
315
+ this._stateChanged.emit();
316
+ // Re-render once the grace window closes so a tag that detection never
317
+ // confirmed (or has since contradicted) stops being shown.
318
+ setTimeout(() => {
319
+ if (!this._isDisposed) {
320
+ this._stateChanged.emit();
321
+ }
322
+ }, LAUNCH_GRACE_MS + 100);
323
+ }
324
+ _onWidgetAdded(_, widget) {
325
+ this._trackWidget(widget);
326
+ this._stateChanged.emit();
327
+ }
328
+ _onShellCurrentChanged() {
329
+ this._updateCurrent();
330
+ }
331
+ /**
332
+ * Recompute which terminal (if any) is the active main-area widget and emit
333
+ * only when it changes. The terminal tracker tells us whether the shell's
334
+ * current widget is one of our terminals; anything else — a notebook, an
335
+ * editor, or nothing — clears the highlight.
336
+ */
337
+ _updateCurrent() {
338
+ var _a, _b;
339
+ const current = (_b = (_a = this._shell) === null || _a === void 0 ? void 0 : _a.currentWidget) !== null && _b !== void 0 ? _b : null;
340
+ const name = current && this._tracker.has(current)
341
+ ? current.content.session.name
342
+ : null;
343
+ if (name !== this._currentName) {
344
+ this._currentName = name;
345
+ this._stateChanged.emit();
346
+ }
347
+ }
348
+ _trackWidget(widget) {
349
+ const name = widget.content.session.name;
350
+ this._cacheLabel(name, widget.title.label);
351
+ widget.title.changed.connect(this._onTitleChanged, this);
352
+ widget.disposed.connect(this._onWidgetDisposed, this);
353
+ }
354
+ _untrackWidget(widget) {
355
+ widget.title.changed.disconnect(this._onTitleChanged, this);
356
+ widget.disposed.disconnect(this._onWidgetDisposed, this);
357
+ }
358
+ _onTitleChanged(title) {
359
+ var _a;
360
+ const owner = title.owner;
361
+ const session = (_a = owner.content) === null || _a === void 0 ? void 0 : _a.session;
362
+ if (!session) {
363
+ return;
364
+ }
365
+ this._cacheLabel(session.name, title.label);
366
+ this._stateChanged.emit();
367
+ }
368
+ /**
369
+ * Update the cached label for a session, but only when the new
370
+ * label is "real" — non-empty and not one of the transient defaults
371
+ * the XTerm widget cycles through during reconnect (`'...'` while
372
+ * the websocket is opening, `'Terminal {name}'` once
373
+ * `_initialConnection` fires). Without the filter, briefly
374
+ * reopening a tab while an agent has yet to re-emit its xterm
375
+ * title escape sequence would clobber the cached agent name with
376
+ * `Terminal 1`, which is exactly the regression we are guarding
377
+ * against. Live widget titles still display whatever the widget
378
+ * currently holds because `labelFor` consults the widget first;
379
+ * the filter only affects what survives a tab close.
380
+ */
381
+ _cacheLabel(name, label) {
382
+ if (!label) {
383
+ return;
384
+ }
385
+ if (label === '...' || label === `Terminal ${name}`) {
386
+ return;
387
+ }
388
+ this._labels.set(name, label);
389
+ }
390
+ _onWidgetDisposed(widget) {
391
+ // The widget is gone but the session may still be running on the
392
+ // server — keep the cached label so the panel keeps the agent's name
393
+ // when the user reopens the tab. We only clean up our subscriptions
394
+ // here; the cache is purged by `runningChanged` once the session
395
+ // itself goes away.
396
+ this._untrackWidget(widget);
397
+ this._stateChanged.emit();
398
+ }
399
+ }
400
+ var Private;
401
+ (function (Private) {
402
+ /**
403
+ * Value-equality for two detection maps, so a poll that changes nothing
404
+ * doesn't trigger a re-render.
405
+ */
406
+ function detectedEqual(a, b) {
407
+ if (a.size !== b.size) {
408
+ return false;
409
+ }
410
+ for (const [name, command] of a) {
411
+ if (!b.has(name) || b.get(name) !== command) {
412
+ return false;
413
+ }
414
+ }
415
+ return true;
416
+ }
417
+ Private.detectedEqual = detectedEqual;
418
+ })(Private || (Private = {}));
@@ -0,0 +1,72 @@
1
+ import { type TranslationBundle } from '@jupyterlab/translation';
2
+ import { LabIcon, ReactWidget } from '@jupyterlab/ui-components';
3
+ import * as React from 'react';
4
+ import { SessionRegistry } from './model';
5
+ /**
6
+ * Id of the panel widget. Used for layout restoration and as the handle
7
+ * the sidebar visibility toggle would target.
8
+ */
9
+ export declare const RUNNING_TERMINALS_ID = "xtralab-running-terminals";
10
+ /**
11
+ * Left-sidebar panel listing every running terminal session. It is the
12
+ * sidebar counterpart of the launcher's terminal tiles: where the
13
+ * launcher *starts* sessions, this panel surfaces the ones already
14
+ * running so the user can jump back to a backgrounded agent — including
15
+ * sessions whose tab has been closed but which are still alive on the
16
+ * server.
17
+ *
18
+ * Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
19
+ * whenever the registry emits `stateChanged`, so it tracks
20
+ * `runningChanged` and the agent-title cache without any local state.
21
+ * Click behaviour (activate vs. reopen, shutdown, new terminal) is
22
+ * delegated to plugin-supplied callbacks so the widget never imports
23
+ * `app`.
24
+ */
25
+ export declare class RunningTerminals extends ReactWidget {
26
+ constructor(options: RunningTerminals.IOptions);
27
+ dispose(): void;
28
+ protected render(): React.ReactElement;
29
+ private _registry;
30
+ private _trans;
31
+ private _iconForCommand;
32
+ private _onActivate;
33
+ private _onShutdown;
34
+ private _onShutdownAll;
35
+ private _onCreate;
36
+ }
37
+ export declare namespace RunningTerminals {
38
+ interface IOptions {
39
+ registry: SessionRegistry;
40
+ trans?: TranslationBundle;
41
+ /**
42
+ * Resolve the running-agent command for a row (from
43
+ * `registry.agentCommandFor`) to the icon to show before its label —
44
+ * the agent's logo, or the plain terminal icon. Supplied by the plugin
45
+ * so the widget never imports the agent list.
46
+ */
47
+ iconForCommand: (command: string | null) => LabIcon;
48
+ /**
49
+ * Activate the named session's open tab, or reopen it in a fresh
50
+ * terminal widget if no tab is currently attached.
51
+ */
52
+ onActivate: (sessionName: string) => void;
53
+ /** Shut the named session down on the server. */
54
+ onShutdown: (sessionName: string) => void;
55
+ /**
56
+ * Shut down every running terminal at once. The plugin is expected to
57
+ * confirm with the user first, since it tears down all live sessions.
58
+ */
59
+ onShutdownAll: () => void;
60
+ /**
61
+ * Activate the "+" button, anchored at the given viewport coordinates
62
+ * (the bottom-left of the button). The plugin decides what to show
63
+ * there — a menu of agents plus a plain terminal, or just a new
64
+ * terminal when no agents are available — so the panel only reports
65
+ * where the button is and never imports the command/menu machinery.
66
+ */
67
+ onCreate: (anchor: {
68
+ x: number;
69
+ y: number;
70
+ }) => void;
71
+ }
72
+ }
@@ -0,0 +1,93 @@
1
+ import { nullTranslator } from '@jupyterlab/translation';
2
+ import { addIcon, closeIcon, ReactWidget, stopIcon, terminalIcon, UseSignal } from '@jupyterlab/ui-components';
3
+ import * as React from 'react';
4
+ /**
5
+ * Id of the panel widget. Used for layout restoration and as the handle
6
+ * the sidebar visibility toggle would target.
7
+ */
8
+ export const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
9
+ /**
10
+ * Left-sidebar panel listing every running terminal session. It is the
11
+ * sidebar counterpart of the launcher's terminal tiles: where the
12
+ * launcher *starts* sessions, this panel surfaces the ones already
13
+ * running so the user can jump back to a backgrounded agent — including
14
+ * sessions whose tab has been closed but which are still alive on the
15
+ * server.
16
+ *
17
+ * Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
18
+ * whenever the registry emits `stateChanged`, so it tracks
19
+ * `runningChanged` and the agent-title cache without any local state.
20
+ * Click behaviour (activate vs. reopen, shutdown, new terminal) is
21
+ * delegated to plugin-supplied callbacks so the widget never imports
22
+ * `app`.
23
+ */
24
+ export class RunningTerminals extends ReactWidget {
25
+ constructor(options) {
26
+ var _a;
27
+ super();
28
+ this._registry = options.registry;
29
+ this._trans = (_a = options.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab');
30
+ this._iconForCommand = options.iconForCommand;
31
+ this._onActivate = options.onActivate;
32
+ this._onShutdown = options.onShutdown;
33
+ this._onShutdownAll = options.onShutdownAll;
34
+ this._onCreate = options.onCreate;
35
+ this.id = RUNNING_TERMINALS_ID;
36
+ this.title.icon = terminalIcon;
37
+ this.title.caption = this._trans.__('Running Terminals');
38
+ this.addClass('jp-xtralab-Terminals');
39
+ }
40
+ dispose() {
41
+ if (this.isDisposed) {
42
+ return;
43
+ }
44
+ // The panel owns the registry, so tear down its upstream
45
+ // subscriptions before the React tree goes away.
46
+ this._registry.dispose();
47
+ super.dispose();
48
+ }
49
+ render() {
50
+ return (React.createElement(UseSignal, { signal: this._registry.stateChanged }, () => (React.createElement(RunningTerminalsComponent, { registry: this._registry, trans: this._trans, iconForCommand: this._iconForCommand, onActivate: this._onActivate, onShutdown: this._onShutdown, onShutdownAll: this._onShutdownAll, onCreate: this._onCreate }))));
51
+ }
52
+ }
53
+ function RunningTerminalsComponent(props) {
54
+ const { registry, trans, iconForCommand, onActivate, onShutdown, onShutdownAll, onCreate } = props;
55
+ const names = registry.sessionNames();
56
+ // The session whose terminal is the current widget in the main area, so its
57
+ // row can be highlighted. `null` when the current tab is a notebook or any
58
+ // other non-terminal widget.
59
+ const currentName = registry.currentSessionName();
60
+ return (React.createElement("div", { className: "jp-xtralab-Terminals-body" },
61
+ React.createElement("div", { className: "jp-xtralab-Terminals-header" },
62
+ React.createElement("h2", { className: "jp-xtralab-Terminals-title" }, trans.__('Terminals')),
63
+ React.createElement("div", { className: "jp-xtralab-Terminals-actions" },
64
+ React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-action jp-xtralab-Terminals-shutdown-all", onClick: onShutdownAll, disabled: names.length === 0, title: trans.__('Shut Down All Terminals'), "aria-label": trans.__('Shut Down All Terminals') },
65
+ React.createElement(stopIcon.react, { tag: "span", verticalAlign: "middle" })),
66
+ React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-action jp-xtralab-Terminals-new", onClick: event => {
67
+ // Anchor whatever the plugin shows (usually an agent menu) to
68
+ // the button's bottom-left, in viewport coordinates — what
69
+ // `Menu.open(x, y)` expects.
70
+ const rect = event.currentTarget.getBoundingClientRect();
71
+ onCreate({ x: rect.left, y: rect.bottom });
72
+ }, title: trans.__('New Terminal'), "aria-label": trans.__('New Terminal'), "aria-haspopup": "menu" },
73
+ React.createElement(addIcon.react, { tag: "span", verticalAlign: "middle" })))),
74
+ names.length === 0 ? (React.createElement("p", { className: "jp-xtralab-Terminals-empty" }, trans.__('No running terminals.'))) : (React.createElement("ul", { className: "jp-xtralab-Terminals-list" }, names.map(name => {
75
+ const label = registry.labelFor(name);
76
+ const hasWidget = registry.widgetFor(name) !== null;
77
+ const tooltip = hasWidget
78
+ ? trans.__('Activate %1', label)
79
+ : trans.__('Reopen %1', label);
80
+ // The running agent's logo (e.g. Claude), or the plain terminal
81
+ // icon when nothing recognised is running in the session.
82
+ const RowIcon = iconForCommand(registry.agentCommandFor(name)).react;
83
+ const isCurrent = name === currentName;
84
+ return (React.createElement("li", { key: name, className: isCurrent
85
+ ? 'jp-xtralab-Terminals-item jp-mod-current'
86
+ : 'jp-xtralab-Terminals-item' },
87
+ React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-activate", onClick: () => onActivate(name), title: tooltip, "aria-label": tooltip, "aria-current": isCurrent || undefined },
88
+ React.createElement(RowIcon, { tag: "span", verticalAlign: "middle" }),
89
+ React.createElement("span", { className: "jp-xtralab-Terminals-item-label" }, label)),
90
+ React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-close", onClick: () => onShutdown(name), title: trans.__('Shut down %1', label), "aria-label": trans.__('Shut down %1', label) },
91
+ React.createElement(closeIcon.react, { tag: "span", verticalAlign: "middle" }))));
92
+ })))));
93
+ }
@@ -0,0 +1,13 @@
1
+ import { LabIcon } from '@jupyterlab/ui-components';
2
+ /**
3
+ * Mirror-symmetric "toggle sidebar" marks for the top-bar buttons, in the
4
+ * same visual language as the macOS / VS Code / svgrepo sidebar icons
5
+ * (https://www.svgrepo.com/vectors/sidebar/): a rounded window outline split
6
+ * by a divider, with the controlled side shaded so left vs. right reads at a
7
+ * glance. The artwork is hand-authored from basic geometry (rounded rect +
8
+ * divider + a shaded panel) so there is no third-party licensing to track,
9
+ * and every stroke/fill uses `currentColor` so the icons inherit the top
10
+ * bar's text color across the light and dark themes.
11
+ */
12
+ export declare const leftSidebarIcon: LabIcon;
13
+ export declare const rightSidebarIcon: LabIcon;
@@ -0,0 +1,27 @@
1
+ import { LabIcon } from '@jupyterlab/ui-components';
2
+ /**
3
+ * Mirror-symmetric "toggle sidebar" marks for the top-bar buttons, in the
4
+ * same visual language as the macOS / VS Code / svgrepo sidebar icons
5
+ * (https://www.svgrepo.com/vectors/sidebar/): a rounded window outline split
6
+ * by a divider, with the controlled side shaded so left vs. right reads at a
7
+ * glance. The artwork is hand-authored from basic geometry (rounded rect +
8
+ * divider + a shaded panel) so there is no third-party licensing to track,
9
+ * and every stroke/fill uses `currentColor` so the icons inherit the top
10
+ * bar's text color across the light and dark themes.
11
+ */
12
+ export const leftSidebarIcon = new LabIcon({
13
+ name: 'xtralab:sidebar-left',
14
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
15
+ <path d="M9 3H5.5A2.5 2.5 0 0 0 3 5.5V18.5A2.5 2.5 0 0 0 5.5 21H9Z" fill="currentColor" fill-opacity="0.3"/>
16
+ <rect x="3" y="3" width="18" height="18" rx="2.5" stroke="currentColor" stroke-width="1.7"/>
17
+ <line x1="9" y1="3" x2="9" y2="21" stroke="currentColor" stroke-width="1.7"/>
18
+ </svg>`
19
+ });
20
+ export const rightSidebarIcon = new LabIcon({
21
+ name: 'xtralab:sidebar-right',
22
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
23
+ <path d="M15 3H18.5A2.5 2.5 0 0 1 21 5.5V18.5A2.5 2.5 0 0 1 18.5 21H15Z" fill="currentColor" fill-opacity="0.3"/>
24
+ <rect x="3" y="3" width="18" height="18" rx="2.5" stroke="currentColor" stroke-width="1.7"/>
25
+ <line x1="15" y1="3" x2="15" y2="21" stroke="currentColor" stroke-width="1.7"/>
26
+ </svg>`
27
+ });
@@ -0,0 +1,16 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ /**
3
+ * Add two icon buttons to the top bar that toggle the left and right side
4
+ * areas, mirroring the macOS-style sidebar buttons: a left-sidebar button at
5
+ * the leading edge and a right-sidebar button at the far edge.
6
+ *
7
+ * Each is a `CommandToolbarButton` wrapping an existing
8
+ * `application:toggle-{left,right}-area` command, so clicking it runs the
9
+ * same toggle as the View menu and the button reflects the command's state —
10
+ * pressed while the matching sidebar is open, and disabled when the area is
11
+ * empty. Note xtralab ships nothing in the right area by default (the
12
+ * property inspector, table of contents and debugger are all disabled), so
13
+ * the right button stays disabled until a widget is moved or added there.
14
+ */
15
+ declare const plugin: JupyterFrontEndPlugin<void>;
16
+ export default plugin;