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
@@ -1,318 +0,0 @@
1
- import type { MainAreaWidget } from '@jupyterlab/apputils';
2
- import type { ServiceManager, Terminal } from '@jupyterlab/services';
3
- import type { ITerminal, ITerminalTracker } from '@jupyterlab/terminal';
4
- import {
5
- type TranslationBundle,
6
- nullTranslator
7
- } from '@jupyterlab/translation';
8
- import {
9
- closeIcon,
10
- terminalIcon,
11
- VDomModel,
12
- VDomRenderer
13
- } from '@jupyterlab/ui-components';
14
- import type { Title, Widget } from '@lumino/widgets';
15
- import * as React from 'react';
16
-
17
- /**
18
- * The widget shape every entry in `ITerminalTracker` takes — kept here
19
- * so the registry's call sites read cleanly.
20
- */
21
- export type TerminalWidget = MainAreaWidget<ITerminal.ITerminal>;
22
-
23
- /**
24
- * Source-of-truth model for the status bar. Each running terminal
25
- * session known to the server is one entry; the registry caches the
26
- * last `widget.title.label` we observed so the agent name (or any
27
- * xterm-published title) survives the user closing the tab while the
28
- * session continues running on the backend.
29
- *
30
- * Two upstream signals feed the model:
31
- * - `serviceManager.terminals.runningChanged` is the authoritative
32
- * list of live sessions; everything not in it has been shut down
33
- * server-side and we must drop it.
34
- * - `tracker.widgetAdded` plus the per-widget `title.changed` /
35
- * `disposed` signals keep our label cache in sync with whatever
36
- * xterm/the launcher has set on the open tabs.
37
- *
38
- * `stateChanged` is reused for both kinds of update so consumers
39
- * (the per-session status item, the plugin's add/remove sync) can
40
- * subscribe once.
41
- */
42
- export class SessionRegistry extends VDomModel {
43
- constructor(
44
- serviceManager: ServiceManager.IManager,
45
- tracker: ITerminalTracker
46
- ) {
47
- super();
48
- this._terminals = serviceManager.terminals;
49
- this._tracker = tracker;
50
-
51
- this._terminals.runningChanged.connect(this._onRunningChanged, this);
52
- this._tracker.widgetAdded.connect(this._onWidgetAdded, this);
53
- this._tracker.forEach(widget => this._trackWidget(widget));
54
- this._refreshLive();
55
- }
56
-
57
- /**
58
- * Names of all live sessions, ordered by their stable rank so the
59
- * status bar items keep their slots as new sessions are added.
60
- */
61
- sessionNames(): string[] {
62
- const names = Array.from(this._live);
63
- names.sort((a, b) => this.rankFor(a) - this.rankFor(b));
64
- return names;
65
- }
66
-
67
- /**
68
- * True iff the named session is still on the server. Used by the
69
- * per-session item to render `null` during the brief window between
70
- * the session's shutdown and the plugin disposing the item.
71
- */
72
- has(name: string): boolean {
73
- return this._live.has(name);
74
- }
75
-
76
- /**
77
- * Display name for the session. The cache wins because it only
78
- * holds "real" labels (the launcher's agent name or an xterm escape
79
- * sequence — see `_cacheLabel` for the filter); reading it first
80
- * prevents the transient `Terminal {name}` an XTerm widget shows
81
- * during reconnect from flickering into the status bar. The live
82
- * widget title is used when no cache exists, and the
83
- * `Terminal {name}` fallback covers sessions we have never seen a
84
- * widget for (e.g. surviving a lab reload).
85
- */
86
- labelFor(name: string): string {
87
- const cached = this._labels.get(name);
88
- if (cached) {
89
- return cached;
90
- }
91
- const widget = this.widgetFor(name);
92
- if (widget && widget.title.label) {
93
- return widget.title.label;
94
- }
95
- return `Terminal ${name}`;
96
- }
97
-
98
- /**
99
- * Return the open widget for a session, if any. The status item
100
- * uses this to switch behaviour between "activate existing tab"
101
- * and "open a new tab connected to the live session".
102
- */
103
- widgetFor(name: string): TerminalWidget | null {
104
- return (
105
- this._tracker.find(widget => widget.content.session.name === name) ?? null
106
- );
107
- }
108
-
109
- /**
110
- * Stable per-session rank assigned in observation order. Reused
111
- * across `registerStatusItem` calls so an item that is briefly
112
- * unregistered (e.g. while a widget is being recreated) lands back
113
- * in the same slot. Cleaned up when the session is shut down so the
114
- * counter does not grow without bound across long sessions.
115
- */
116
- rankFor(name: string): number {
117
- let rank = this._ranks.get(name);
118
- if (rank === undefined) {
119
- rank = this._nextRank++;
120
- this._ranks.set(name, rank);
121
- }
122
- return rank;
123
- }
124
-
125
- dispose(): void {
126
- if (this.isDisposed) {
127
- return;
128
- }
129
- this._terminals.runningChanged.disconnect(this._onRunningChanged, this);
130
- this._tracker.widgetAdded.disconnect(this._onWidgetAdded, this);
131
- this._tracker.forEach(widget => this._untrackWidget(widget));
132
- super.dispose();
133
- }
134
-
135
- private _refreshLive(): void {
136
- const next = new Set<string>();
137
- for (const model of this._terminals.running()) {
138
- next.add(model.name);
139
- }
140
- this._live = next;
141
- // Drop cache + rank entries for sessions that have been shut down
142
- // server-side. Without this prune the rank counter would keep
143
- // climbing across long-running labs as terminals come and go.
144
- for (const name of Array.from(this._labels.keys())) {
145
- if (!next.has(name)) {
146
- this._labels.delete(name);
147
- }
148
- }
149
- for (const name of Array.from(this._ranks.keys())) {
150
- if (!next.has(name)) {
151
- this._ranks.delete(name);
152
- }
153
- }
154
- }
155
-
156
- private _onRunningChanged(_: unknown, sessions: Terminal.IModel[]): void {
157
- this._live = new Set(sessions.map(model => model.name));
158
- for (const name of Array.from(this._labels.keys())) {
159
- if (!this._live.has(name)) {
160
- this._labels.delete(name);
161
- }
162
- }
163
- for (const name of Array.from(this._ranks.keys())) {
164
- if (!this._live.has(name)) {
165
- this._ranks.delete(name);
166
- }
167
- }
168
- this.stateChanged.emit();
169
- }
170
-
171
- private _onWidgetAdded(_: unknown, widget: TerminalWidget): void {
172
- this._trackWidget(widget);
173
- this.stateChanged.emit();
174
- }
175
-
176
- private _trackWidget(widget: TerminalWidget): void {
177
- const name = widget.content.session.name;
178
- this._cacheLabel(name, widget.title.label);
179
- widget.title.changed.connect(this._onTitleChanged, this);
180
- widget.disposed.connect(this._onWidgetDisposed, this);
181
- }
182
-
183
- private _untrackWidget(widget: TerminalWidget): void {
184
- widget.title.changed.disconnect(this._onTitleChanged, this);
185
- widget.disposed.disconnect(this._onWidgetDisposed, this);
186
- }
187
-
188
- private _onTitleChanged(title: Title<Widget>): void {
189
- const owner = title.owner as TerminalWidget;
190
- const session = owner.content?.session;
191
- if (!session) {
192
- return;
193
- }
194
- this._cacheLabel(session.name, title.label);
195
- this.stateChanged.emit();
196
- }
197
-
198
- /**
199
- * Update the cached label for a session, but only when the new
200
- * label is "real" — non-empty and not one of the transient defaults
201
- * the XTerm widget cycles through during reconnect (`'...'` while
202
- * the websocket is opening, `'Terminal {name}'` once
203
- * `_initialConnection` fires). Without the filter, briefly
204
- * reopening a tab while an agent has yet to re-emit its xterm
205
- * title escape sequence would clobber the cached agent name with
206
- * `Terminal 1`, which is exactly the regression we are guarding
207
- * against. Live widget titles still display whatever the widget
208
- * currently holds because `labelFor` consults the widget first;
209
- * the filter only affects what survives a tab close.
210
- */
211
- private _cacheLabel(name: string, label: string): void {
212
- if (!label) {
213
- return;
214
- }
215
- if (label === '...' || label === `Terminal ${name}`) {
216
- return;
217
- }
218
- this._labels.set(name, label);
219
- }
220
-
221
- private _onWidgetDisposed(widget: Widget): void {
222
- // The widget is gone but the session may still be running on the
223
- // server — keep the cached label so the status item keeps the
224
- // agent's name when the user reopens the tab. We only clean up
225
- // our subscriptions here; the cache is purged by `runningChanged`
226
- // once the session itself goes away.
227
- this._untrackWidget(widget as TerminalWidget);
228
- this.stateChanged.emit();
229
- }
230
-
231
- private _terminals: Terminal.IManager;
232
- private _tracker: ITerminalTracker;
233
- private _labels = new Map<string, string>();
234
- private _ranks = new Map<string, number>();
235
- private _live = new Set<string>();
236
- private _nextRank = 100;
237
- }
238
-
239
- /**
240
- * One status bar item per live terminal session. Renders a terminal
241
- * icon plus the session's label, with an inline shutdown button that
242
- * appears on hover/focus. The item delegates click handling to the
243
- * plugin so the lifecycle (create/dispose) and the user-facing
244
- * behaviour (activate vs. reopen, shutdown) live in one place.
245
- */
246
- export class TerminalSessionItem extends VDomRenderer<SessionRegistry> {
247
- constructor(options: TerminalSessionItem.IOptions) {
248
- super(options.registry);
249
- this._sessionName = options.sessionName;
250
- this._trans = options.trans ?? nullTranslator.load('jupyterlab');
251
- this._onActivate = options.onActivate;
252
- this._onShutdown = options.onShutdown;
253
- this.addClass('jp-mod-highlighted');
254
- this.addClass('jp-xtralab-StatusBar-Terminal');
255
- }
256
-
257
- render(): React.ReactElement | null {
258
- if (!this.model || !this.model.has(this._sessionName)) {
259
- return null;
260
- }
261
- const trans = this._trans;
262
- const label = this.model.labelFor(this._sessionName);
263
- const hasWidget = this.model.widgetFor(this._sessionName) !== null;
264
- const tooltip = hasWidget
265
- ? trans.__('Activate %1', label)
266
- : trans.__('Reopen %1', label);
267
- this.node.title = tooltip;
268
-
269
- return (
270
- <div className="jp-xtralab-StatusBar-Terminal-Container">
271
- <button
272
- type="button"
273
- className="jp-xtralab-StatusBar-Terminal-Activate"
274
- onClick={() => this._onActivate(this._sessionName)}
275
- aria-label={tooltip}
276
- >
277
- <terminalIcon.react
278
- tag="span"
279
- verticalAlign="middle"
280
- stylesheet="statusBar"
281
- />
282
- <span className="jp-xtralab-StatusBar-Terminal-Label">{label}</span>
283
- </button>
284
- <button
285
- type="button"
286
- className="jp-xtralab-StatusBar-Terminal-Close"
287
- onClick={event => {
288
- event.stopPropagation();
289
- this._onShutdown(this._sessionName);
290
- }}
291
- title={trans.__('Shut down %1', label)}
292
- aria-label={trans.__('Shut down %1', label)}
293
- >
294
- <closeIcon.react
295
- tag="span"
296
- verticalAlign="middle"
297
- stylesheet="statusBar"
298
- />
299
- </button>
300
- </div>
301
- );
302
- }
303
-
304
- private _sessionName: string;
305
- private _trans: TranslationBundle;
306
- private _onActivate: (sessionName: string) => void;
307
- private _onShutdown: (sessionName: string) => void;
308
- }
309
-
310
- export namespace TerminalSessionItem {
311
- export interface IOptions {
312
- registry: SessionRegistry;
313
- sessionName: string;
314
- trans?: TranslationBundle;
315
- onActivate: (sessionName: string) => void;
316
- onShutdown: (sessionName: string) => void;
317
- }
318
- }
@@ -1,95 +0,0 @@
1
- /*
2
- * Per-session terminal items on the status bar. Each session gets its
3
- * own item, so the rules below scope tightly under
4
- * `jp-xtralab-StatusBar-Terminal` to avoid leaking into other items
5
- * registered by upstream extensions on the same bar.
6
- *
7
- * Colors and spacing pull from `--jp-*` custom properties so the items
8
- * track the active JupyterLab theme.
9
- */
10
-
11
- .jp-xtralab-StatusBar-Terminal {
12
- /* Each item is a single horizontal capsule. The host element comes
13
- * from the status bar layout — we scope our styles to its inner
14
- * container so the bar's own padding/margins are respected. */
15
- display: flex;
16
- align-items: center;
17
- }
18
-
19
- .jp-xtralab-StatusBar-Terminal-Container {
20
- display: flex;
21
- align-items: stretch;
22
- height: 100%;
23
- }
24
-
25
- /*
26
- * Activate button. Stretches the full row so the icon + label are
27
- * one click target; the close button sits to the right and intercepts
28
- * clicks on its own area via stopPropagation in the React handler.
29
- */
30
- .jp-xtralab-StatusBar-Terminal-Activate {
31
- display: flex;
32
- align-items: center;
33
- gap: 4px;
34
- padding: 0 6px;
35
- background: transparent;
36
- border: none;
37
- color: inherit;
38
- font: inherit;
39
- cursor: pointer;
40
- min-width: 0;
41
- }
42
-
43
- .jp-xtralab-StatusBar-Terminal-Activate:focus-visible {
44
- outline: var(--jp-border-width) solid var(--jp-brand-color1);
45
- outline-offset: -2px;
46
- }
47
-
48
- .jp-xtralab-StatusBar-Terminal-Label {
49
- /* Cap each label so a verbose xterm title (Claude can include the
50
- * cwd, branch, etc.) does not push every other status item off the
51
- * bar. The truncation tooltip falls back to the full label via the
52
- * surrounding container's `title` attribute. */
53
- max-width: 160px;
54
- overflow: hidden;
55
- text-overflow: ellipsis;
56
- white-space: nowrap;
57
- font-size: var(--jp-ui-font-size1);
58
- }
59
-
60
- /*
61
- * Close button. Hidden by default so the row reads as plain text;
62
- * fades in when the row is hovered or the button itself takes focus.
63
- * No background-color change on hover — the outer item already
64
- * shifts to `--jp-layout-color3` via `jp-mod-highlighted:hover`, and
65
- * stacking another shade on top of that produced an unpleasant frame
66
- * around the inner content.
67
- */
68
- .jp-xtralab-StatusBar-Terminal-Close {
69
- display: flex;
70
- align-items: center;
71
- justify-content: center;
72
- width: 18px;
73
- margin-right: 2px;
74
- background: transparent;
75
- border: none;
76
- color: var(--jp-ui-font-color2);
77
- cursor: pointer;
78
- opacity: 0;
79
- transition: opacity 80ms linear;
80
- }
81
-
82
- .jp-xtralab-StatusBar-Terminal-Close:hover {
83
- color: var(--jp-ui-font-color1);
84
- }
85
-
86
- .jp-xtralab-StatusBar-Terminal-Close:focus-visible {
87
- opacity: 1;
88
- outline: var(--jp-border-width) solid var(--jp-brand-color1);
89
- outline-offset: -2px;
90
- }
91
-
92
- .jp-xtralab-StatusBar-Terminal-Container:hover
93
- .jp-xtralab-StatusBar-Terminal-Close {
94
- opacity: 1;
95
- }