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,320 +0,0 @@
1
- import * as React from 'react';
2
- import { Poll } from '@lumino/polling';
3
- import { add, addAll, checkout, porcelainToFileChanges, reset, resetAll, showTopLevel, status } from './api';
4
- /**
5
- * The polling interval for the panel. We refresh the status periodically so
6
- * out-of-band changes (e.g. the user runs `git add` from a terminal) become
7
- * visible without requiring a manual refresh.
8
- */
9
- const STATUS_POLL_INTERVAL_MS = 5000;
10
- /**
11
- * Upper bound on the poll's exponential backoff when status requests fail
12
- * repeatedly. Matches the default JupyterLab file browser's `max`: long
13
- * enough that a server outage stops hammering the API, short enough that
14
- * a transient failure heals on its own.
15
- */
16
- const STATUS_POLL_MAX_MS = 300000;
17
- /**
18
- * Top-level React component rendered inside the {@link GitPanel} widget.
19
- * Owns the polling + refresh wiring around the `/git/status` endpoint and
20
- * the small bit of UI state that controls section collapse and the
21
- * in-flight per-file action spinners.
22
- */
23
- export function GitPanelComponent(props) {
24
- const { handlers, refreshSignal } = props;
25
- const [state, setState] = React.useState({
26
- loading: true,
27
- result: null,
28
- error: null
29
- });
30
- // The absolute filesystem path of the resolved git top-level. Used purely
31
- // for display in the panel header.
32
- const [topLevel, setTopLevel] = React.useState(null);
33
- // Per-action spinner gating: keyed by `${group}:${path}`, value is the
34
- // action currently in flight. Lets the row render a disabled state on
35
- // exactly the file that is being mutated, without locking the whole panel.
36
- const [pendingActions, setPendingActions] = React.useState({});
37
- const [stagedCollapsed, setStagedCollapsed] = React.useState(false);
38
- const [changesCollapsed, setChangesCollapsed] = React.useState(false);
39
- // The repo path passed to every API call. We use the JupyterLab server's
40
- // root (the empty string) and let `git` resolve the enclosing repo on the
41
- // server side. This works as long as the server was started inside a git
42
- // repo — the common case.
43
- const repoPath = '';
44
- const refresh = React.useCallback(async () => {
45
- var _a;
46
- try {
47
- const [topLevelResult, statusResult] = await Promise.all([
48
- showTopLevel(repoPath),
49
- status(repoPath)
50
- ]);
51
- setTopLevel(topLevelResult);
52
- if (statusResult.code !== 0) {
53
- setState({
54
- loading: false,
55
- result: null,
56
- error: (_a = statusResult.message) !== null && _a !== void 0 ? _a : 'Failed to load git status'
57
- });
58
- return;
59
- }
60
- setState({ loading: false, result: statusResult, error: null });
61
- }
62
- catch (err) {
63
- setState({
64
- loading: false,
65
- result: null,
66
- error: err instanceof Error ? err.message : String(err)
67
- });
68
- }
69
- }, [repoPath]);
70
- // Initial load + polling. Driven by a Lumino `Poll` so the panel stops
71
- // hitting the git endpoint while the JupyterLab tab is hidden, and so
72
- // repeated failures back off exponentially instead of spinning at 5s.
73
- // External refresh triggers (toolbar button, plugin-level events) call
74
- // `poll.refresh()` to schedule an immediate tick instead of running
75
- // `refresh()` directly — that keeps the panel from issuing two parallel
76
- // status requests when an automatic tick happens to land at the same
77
- // time as an external trigger.
78
- React.useEffect(() => {
79
- const poll = new Poll({
80
- name: '@xtralab/git:panelStatus',
81
- factory: () => refresh(),
82
- frequency: {
83
- interval: STATUS_POLL_INTERVAL_MS,
84
- backoff: true,
85
- max: STATUS_POLL_MAX_MS
86
- },
87
- standby: 'when-hidden'
88
- });
89
- const handler = () => {
90
- void poll.refresh();
91
- };
92
- refreshSignal.connect(handler);
93
- return () => {
94
- refreshSignal.disconnect(handler);
95
- poll.dispose();
96
- };
97
- }, [refresh, refreshSignal]);
98
- const runAction = React.useCallback(async (change, action, run) => {
99
- const key = `${change.group}:${change.path}`;
100
- setPendingActions(prev => ({ ...prev, [key]: action }));
101
- try {
102
- await run();
103
- await refresh();
104
- }
105
- catch (err) {
106
- await handlers.showError('Git operation failed', err instanceof Error ? err : new Error(String(err)));
107
- }
108
- finally {
109
- setPendingActions(prev => {
110
- const next = { ...prev };
111
- delete next[key];
112
- return next;
113
- });
114
- }
115
- }, [handlers, refresh]);
116
- const onStage = React.useCallback((change) => runAction(change, 'stage', () => add(repoPath, change.path)), [repoPath, runAction]);
117
- const onUnstage = React.useCallback((change) => runAction(change, 'unstage', () => reset(repoPath, change.path)), [repoPath, runAction]);
118
- const onDiscard = React.useCallback(async (change) => {
119
- const verb = change.status === 'untracked' ? 'remove' : 'restore';
120
- const accepted = await handlers.confirm(verb === 'remove' ? 'Remove file' : 'Discard changes', verb === 'remove'
121
- ? `Permanently remove ${change.path}? This cannot be undone.`
122
- : `Discard all changes to ${change.path}? This will revert the file to its last staged version and cannot be undone.`, verb === 'remove' ? 'Remove' : 'Discard');
123
- if (!accepted) {
124
- return;
125
- }
126
- // The git extension's `checkout` endpoint without a branch reverts the
127
- // working tree copy of the file to the index. For untracked files we
128
- // can't checkout, so the user is told the action is unsupported and we
129
- // bail out — they can delete the file via the file browser instead.
130
- if (change.status === 'untracked') {
131
- await handlers.showError('Cannot discard untracked file', new Error('Untracked files cannot be discarded from the git panel — delete them from the file browser instead.'));
132
- return;
133
- }
134
- await runAction(change, 'discard', () => checkout(repoPath, change.path));
135
- }, [handlers, repoPath, runAction]);
136
- const onStageAll = React.useCallback(async () => {
137
- try {
138
- await addAll(repoPath);
139
- await refresh();
140
- }
141
- catch (err) {
142
- await handlers.showError('Stage all failed', err instanceof Error ? err : new Error(String(err)));
143
- }
144
- }, [handlers, refresh, repoPath]);
145
- const onUnstageAll = React.useCallback(async () => {
146
- try {
147
- await resetAll(repoPath);
148
- await refresh();
149
- }
150
- catch (err) {
151
- await handlers.showError('Unstage all failed', err instanceof Error ? err : new Error(String(err)));
152
- }
153
- }, [handlers, refresh, repoPath]);
154
- // Categorize the porcelain entries into the two visible groups. Because
155
- // `porcelainToFileChanges` may emit two changes per file (one staged, one
156
- // unstaged), the same file can show up in both lists when it has been
157
- // partially staged.
158
- const { staged, unstaged } = React.useMemo(() => {
159
- const stagedAcc = [];
160
- const unstagedAcc = [];
161
- if (state.result === null) {
162
- return { staged: stagedAcc, unstaged: unstagedAcc };
163
- }
164
- for (const file of state.result.files) {
165
- for (const change of porcelainToFileChanges(file)) {
166
- if (change.group === 'staged') {
167
- stagedAcc.push(change);
168
- }
169
- else {
170
- unstagedAcc.push(change);
171
- }
172
- }
173
- }
174
- stagedAcc.sort((a, b) => a.path.localeCompare(b.path));
175
- unstagedAcc.sort((a, b) => a.path.localeCompare(b.path));
176
- return { staged: stagedAcc, unstaged: unstagedAcc };
177
- }, [state.result]);
178
- return (React.createElement("div", { className: "jp-xtralab-GitPanel-content" },
179
- React.createElement(PanelHeader, { topLevel: topLevel, result: state.result, loading: state.loading, error: state.error, onRefresh: () => void refresh() }),
180
- state.error !== null ? (React.createElement("div", { className: "jp-xtralab-GitPanel-error" }, state.error)) : (React.createElement(React.Fragment, null,
181
- React.createElement(ChangeSection, { label: "Staged Changes", count: staged.length, collapsed: stagedCollapsed, onToggleCollapsed: () => setStagedCollapsed(c => !c), actions: staged.length > 0
182
- ? [
183
- {
184
- label: 'Unstage all changes',
185
- symbol: '−',
186
- onClick: onUnstageAll
187
- }
188
- ]
189
- : undefined }, staged.map(change => (React.createElement(ChangeRow, { key: `${change.group}:${change.path}`, change: change, pendingAction: pendingActions[`${change.group}:${change.path}`], onClick: event => handlers.openDiff(change, {
190
- pin: event.ctrlKey || event.metaKey
191
- }), actions: [
192
- {
193
- label: 'Unstage changes',
194
- symbol: '−',
195
- onClick: () => void onUnstage(change)
196
- }
197
- ] })))),
198
- React.createElement(ChangeSection, { label: "Changes", count: unstaged.length, collapsed: changesCollapsed, onToggleCollapsed: () => setChangesCollapsed(c => !c), actions: unstaged.length > 0
199
- ? [
200
- {
201
- label: 'Stage all changes',
202
- symbol: '+',
203
- onClick: onStageAll
204
- }
205
- ]
206
- : undefined }, unstaged.map(change => (React.createElement(ChangeRow, { key: `${change.group}:${change.path}`, change: change, pendingAction: pendingActions[`${change.group}:${change.path}`], onClick: event => handlers.openDiff(change, {
207
- pin: event.ctrlKey || event.metaKey
208
- }), actions: [
209
- {
210
- label: 'Discard changes',
211
- symbol: '⟲',
212
- onClick: () => void onDiscard(change)
213
- },
214
- {
215
- label: 'Stage changes',
216
- symbol: '+',
217
- onClick: () => void onStage(change)
218
- }
219
- ] })))),
220
- staged.length === 0 && unstaged.length === 0 && !state.loading ? (React.createElement("div", { className: "jp-xtralab-GitPanel-empty" }, "No changes detected.")) : null))));
221
- }
222
- function PanelHeader(props) {
223
- var _a;
224
- const { topLevel, result, loading, error, onRefresh } = props;
225
- const repoName = topLevel !== null
226
- ? ((_a = topLevel
227
- .split('/')
228
- .filter(s => s.length > 0)
229
- .pop()) !== null && _a !== void 0 ? _a : topLevel)
230
- : null;
231
- return (React.createElement("div", { className: "jp-xtralab-GitPanel-header" },
232
- React.createElement("div", { className: "jp-xtralab-GitPanel-headerInfo" },
233
- repoName !== null ? (React.createElement("div", { className: "jp-xtralab-GitPanel-repo", title: topLevel !== null && topLevel !== void 0 ? topLevel : '' }, repoName)) : null,
234
- (result === null || result === void 0 ? void 0 : result.branch) !== undefined && (result === null || result === void 0 ? void 0 : result.branch) !== null ? (React.createElement("div", { className: "jp-xtralab-GitPanel-branch" },
235
- result.branch,
236
- result.ahead > 0 ? React.createElement("span", null,
237
- " \u2191",
238
- result.ahead) : null,
239
- result.behind > 0 ? React.createElement("span", null,
240
- " \u2193",
241
- result.behind) : null)) : null),
242
- React.createElement("button", { type: "button", className: "jp-xtralab-GitPanel-iconButton", title: "Refresh", disabled: loading && error === null, onClick: onRefresh }, "\u27F3")));
243
- }
244
- function ChangeSection(props) {
245
- const { label, count, collapsed, onToggleCollapsed, actions, children } = props;
246
- return (React.createElement("div", { className: "jp-xtralab-GitPanel-section" },
247
- React.createElement("div", { className: "jp-xtralab-GitPanel-sectionHeader", onClick: onToggleCollapsed },
248
- React.createElement("span", { className: "jp-xtralab-GitPanel-sectionChevron" }, collapsed ? '▸' : '▾'),
249
- React.createElement("span", { className: "jp-xtralab-GitPanel-sectionLabel" }, label),
250
- actions !== undefined ? (React.createElement("span", { className: "jp-xtralab-GitPanel-sectionActions" }, actions.map(action => (React.createElement("button", { key: action.label, type: "button", className: "jp-xtralab-GitPanel-iconButton", title: action.label, onClick: event => {
251
- event.stopPropagation();
252
- action.onClick();
253
- } }, action.symbol))))) : null,
254
- React.createElement("span", { className: "jp-xtralab-GitPanel-sectionCount" }, count)),
255
- !collapsed ? (React.createElement("div", { className: "jp-xtralab-GitPanel-sectionBody" }, children)) : null));
256
- }
257
- function ChangeRow(props) {
258
- const { change, pendingAction, onClick, actions } = props;
259
- const filename = basename(change.path);
260
- const dirname = dirnameOrEmpty(change.path);
261
- return (React.createElement("div", { className: "jp-xtralab-GitPanel-row", "data-status": change.status, title: change.from !== undefined
262
- ? `${change.from} → ${change.path}`
263
- : change.path, onClick: onClick },
264
- React.createElement("span", { className: "jp-xtralab-GitPanel-rowLabel" },
265
- React.createElement("span", { className: "jp-xtralab-GitPanel-rowName" }, filename),
266
- dirname !== '' ? (React.createElement("span", { className: "jp-xtralab-GitPanel-rowPath" }, dirname)) : null),
267
- React.createElement("span", { className: "jp-xtralab-GitPanel-rowActions", "data-pending": pendingAction !== undefined }, actions.map(action => (React.createElement("button", { key: action.label, type: "button", className: "jp-xtralab-GitPanel-iconButton", title: action.label, disabled: pendingAction !== undefined, onClick: event => {
268
- event.stopPropagation();
269
- action.onClick();
270
- } }, action.symbol)))),
271
- React.createElement("span", { className: "jp-xtralab-GitPanel-rowBadge", "data-status": change.status, title: statusTooltip(change.status) }, statusBadge(change.status))));
272
- }
273
- function statusBadge(status) {
274
- switch (status) {
275
- case 'modified':
276
- return 'M';
277
- case 'added':
278
- return 'A';
279
- case 'deleted':
280
- return 'D';
281
- case 'renamed':
282
- return 'R';
283
- case 'untracked':
284
- return 'U';
285
- case 'unmerged':
286
- return '!';
287
- case 'typechange':
288
- return 'T';
289
- default:
290
- return '?';
291
- }
292
- }
293
- function statusTooltip(status) {
294
- switch (status) {
295
- case 'modified':
296
- return 'Modified';
297
- case 'added':
298
- return 'Added';
299
- case 'deleted':
300
- return 'Deleted';
301
- case 'renamed':
302
- return 'Renamed';
303
- case 'untracked':
304
- return 'Untracked';
305
- case 'unmerged':
306
- return 'Unmerged (conflict)';
307
- case 'typechange':
308
- return 'Type changed';
309
- default:
310
- return 'Unknown';
311
- }
312
- }
313
- function basename(path) {
314
- const idx = path.lastIndexOf('/');
315
- return idx < 0 ? path : path.slice(idx + 1);
316
- }
317
- function dirnameOrEmpty(path) {
318
- const idx = path.lastIndexOf('/');
319
- return idx < 0 ? '' : path.slice(0, idx);
320
- }
@@ -1,14 +0,0 @@
1
- import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
- /**
3
- * Surfaces every running terminal session as its own item on the left
4
- * side of the status bar. The list is driven by
5
- * `serviceManager.terminals.runningChanged`, not by the widget tracker,
6
- * so a session that has its tab closed but is still running on the
7
- * backend keeps its slot — the typical state for an agent the user has
8
- * stepped away from. Clicking an item activates the existing tab if one
9
- * is open, or reopens the session in a fresh terminal widget. The
10
- * inline `×` button shuts the session down on the server, which
11
- * cascades through the widget if it is currently open.
12
- */
13
- declare const plugin: JupyterFrontEndPlugin<void>;
14
- export default plugin;
@@ -1,89 +0,0 @@
1
- import { IStatusBar } from '@jupyterlab/statusbar';
2
- import { ITerminalTracker } from '@jupyterlab/terminal';
3
- import { ITranslator, nullTranslator } from '@jupyterlab/translation';
4
- import { DisposableSet } from '@lumino/disposable';
5
- import { SessionRegistry, TerminalSessionItem } from './widget';
6
- const PLUGIN_ID = 'xtralab:statusBar';
7
- /**
8
- * Surfaces every running terminal session as its own item on the left
9
- * side of the status bar. The list is driven by
10
- * `serviceManager.terminals.runningChanged`, not by the widget tracker,
11
- * so a session that has its tab closed but is still running on the
12
- * backend keeps its slot — the typical state for an agent the user has
13
- * stepped away from. Clicking an item activates the existing tab if one
14
- * is open, or reopens the session in a fresh terminal widget. The
15
- * inline `×` button shuts the session down on the server, which
16
- * cascades through the widget if it is currently open.
17
- */
18
- const plugin = {
19
- id: PLUGIN_ID,
20
- description: 'Surface each running terminal session as its own status bar item so users can jump to (or reopen) backgrounded agents from anywhere in the workspace.',
21
- autoStart: true,
22
- requires: [IStatusBar, ITerminalTracker],
23
- optional: [ITranslator],
24
- activate: (app, statusBar, tracker, translator) => {
25
- const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
26
- const registry = new SessionRegistry(app.serviceManager, tracker);
27
- // One DisposableSet per session — registerStatusItem returns an
28
- // IDisposable for the registration, and the item widget itself
29
- // implements IDisposable. Bundling them lets a single `dispose()`
30
- // tear the slot down completely when the session goes away.
31
- const items = new Map();
32
- const onActivate = (name) => {
33
- // `terminal:open` is the upstream entry point that handles both
34
- // sides of this: if a widget is already attached to the named
35
- // session it activates that tab, otherwise it spins up a fresh
36
- // widget connected to the live session. Reusing it keeps the
37
- // path identical to a click in the running-sessions sidebar.
38
- void app.commands.execute('terminal:open', { name });
39
- };
40
- const onShutdown = (name) => {
41
- void app.serviceManager.terminals.shutdown(name).catch(reason => {
42
- console.error(`xtralab: failed to shut down terminal ${name}: ${reason}`);
43
- });
44
- };
45
- const sync = () => {
46
- const live = new Set(registry.sessionNames());
47
- // Add items for newly-seen sessions. We register each at its
48
- // stable rank from the registry so re-creating an item between
49
- // syncs (e.g. if we ever decide to swap representations) lands
50
- // in the same slot.
51
- for (const name of live) {
52
- if (items.has(name)) {
53
- continue;
54
- }
55
- const item = new TerminalSessionItem({
56
- registry,
57
- sessionName: name,
58
- trans,
59
- onActivate,
60
- onShutdown
61
- });
62
- const registration = statusBar.registerStatusItem(`${PLUGIN_ID}:${name}`, {
63
- item,
64
- align: 'left',
65
- rank: registry.rankFor(name)
66
- });
67
- const set = new DisposableSet();
68
- set.add(registration);
69
- set.add(item);
70
- items.set(name, set);
71
- }
72
- // Remove items for sessions the server no longer knows about.
73
- for (const [name, disposable] of Array.from(items.entries())) {
74
- if (!live.has(name)) {
75
- disposable.dispose();
76
- items.delete(name);
77
- }
78
- }
79
- };
80
- registry.stateChanged.connect(sync);
81
- void app.serviceManager.terminals.ready.then(() => {
82
- // The first `runningChanged` may have fired before our connect,
83
- // depending on how the manager initialises — refresh once after
84
- // it is ready so any pre-existing sessions get their items.
85
- sync();
86
- });
87
- }
88
- };
89
- export default plugin;
@@ -1,121 +0,0 @@
1
- import type { MainAreaWidget } from '@jupyterlab/apputils';
2
- import type { ServiceManager } from '@jupyterlab/services';
3
- import type { ITerminal, ITerminalTracker } from '@jupyterlab/terminal';
4
- import { type TranslationBundle } from '@jupyterlab/translation';
5
- import { VDomModel, VDomRenderer } from '@jupyterlab/ui-components';
6
- import * as React from 'react';
7
- /**
8
- * The widget shape every entry in `ITerminalTracker` takes — kept here
9
- * so the registry's call sites read cleanly.
10
- */
11
- export type TerminalWidget = MainAreaWidget<ITerminal.ITerminal>;
12
- /**
13
- * Source-of-truth model for the status bar. Each running terminal
14
- * session known to the server is one entry; the registry caches the
15
- * last `widget.title.label` we observed so the agent name (or any
16
- * xterm-published title) survives the user closing the tab while the
17
- * session continues running on the backend.
18
- *
19
- * Two upstream signals feed the model:
20
- * - `serviceManager.terminals.runningChanged` is the authoritative
21
- * list of live sessions; everything not in it has been shut down
22
- * server-side and we must drop it.
23
- * - `tracker.widgetAdded` plus the per-widget `title.changed` /
24
- * `disposed` signals keep our label cache in sync with whatever
25
- * xterm/the launcher has set on the open tabs.
26
- *
27
- * `stateChanged` is reused for both kinds of update so consumers
28
- * (the per-session status item, the plugin's add/remove sync) can
29
- * subscribe once.
30
- */
31
- export declare class SessionRegistry extends VDomModel {
32
- constructor(serviceManager: ServiceManager.IManager, tracker: ITerminalTracker);
33
- /**
34
- * Names of all live sessions, ordered by their stable rank so the
35
- * status bar items keep their slots as new sessions are added.
36
- */
37
- sessionNames(): string[];
38
- /**
39
- * True iff the named session is still on the server. Used by the
40
- * per-session item to render `null` during the brief window between
41
- * the session's shutdown and the plugin disposing the item.
42
- */
43
- has(name: string): boolean;
44
- /**
45
- * Display name for the session. The cache wins because it only
46
- * holds "real" labels (the launcher's agent name or an xterm escape
47
- * sequence — see `_cacheLabel` for the filter); reading it first
48
- * prevents the transient `Terminal {name}` an XTerm widget shows
49
- * during reconnect from flickering into the status bar. The live
50
- * widget title is used when no cache exists, and the
51
- * `Terminal {name}` fallback covers sessions we have never seen a
52
- * widget for (e.g. surviving a lab reload).
53
- */
54
- labelFor(name: string): string;
55
- /**
56
- * Return the open widget for a session, if any. The status item
57
- * uses this to switch behaviour between "activate existing tab"
58
- * and "open a new tab connected to the live session".
59
- */
60
- widgetFor(name: string): TerminalWidget | null;
61
- /**
62
- * Stable per-session rank assigned in observation order. Reused
63
- * across `registerStatusItem` calls so an item that is briefly
64
- * unregistered (e.g. while a widget is being recreated) lands back
65
- * in the same slot. Cleaned up when the session is shut down so the
66
- * counter does not grow without bound across long sessions.
67
- */
68
- rankFor(name: string): number;
69
- dispose(): void;
70
- private _refreshLive;
71
- private _onRunningChanged;
72
- private _onWidgetAdded;
73
- private _trackWidget;
74
- private _untrackWidget;
75
- private _onTitleChanged;
76
- /**
77
- * Update the cached label for a session, but only when the new
78
- * label is "real" — non-empty and not one of the transient defaults
79
- * the XTerm widget cycles through during reconnect (`'...'` while
80
- * the websocket is opening, `'Terminal {name}'` once
81
- * `_initialConnection` fires). Without the filter, briefly
82
- * reopening a tab while an agent has yet to re-emit its xterm
83
- * title escape sequence would clobber the cached agent name with
84
- * `Terminal 1`, which is exactly the regression we are guarding
85
- * against. Live widget titles still display whatever the widget
86
- * currently holds because `labelFor` consults the widget first;
87
- * the filter only affects what survives a tab close.
88
- */
89
- private _cacheLabel;
90
- private _onWidgetDisposed;
91
- private _terminals;
92
- private _tracker;
93
- private _labels;
94
- private _ranks;
95
- private _live;
96
- private _nextRank;
97
- }
98
- /**
99
- * One status bar item per live terminal session. Renders a terminal
100
- * icon plus the session's label, with an inline shutdown button that
101
- * appears on hover/focus. The item delegates click handling to the
102
- * plugin so the lifecycle (create/dispose) and the user-facing
103
- * behaviour (activate vs. reopen, shutdown) live in one place.
104
- */
105
- export declare class TerminalSessionItem extends VDomRenderer<SessionRegistry> {
106
- constructor(options: TerminalSessionItem.IOptions);
107
- render(): React.ReactElement | null;
108
- private _sessionName;
109
- private _trans;
110
- private _onActivate;
111
- private _onShutdown;
112
- }
113
- export declare namespace TerminalSessionItem {
114
- interface IOptions {
115
- registry: SessionRegistry;
116
- sessionName: string;
117
- trans?: TranslationBundle;
118
- onActivate: (sessionName: string) => void;
119
- onShutdown: (sessionName: string) => void;
120
- }
121
- }