xtralab 0.14.1 → 0.15.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/lib/customPanel/index.d.ts +7 -0
- package/lib/customPanel/index.js +74 -0
- package/lib/fileBrowser/commands.js +13 -1
- package/lib/fileBrowser/contents.d.ts +11 -0
- package/lib/fileBrowser/contents.js +28 -0
- package/lib/fileBrowser/dragAndDrop.d.ts +66 -0
- package/lib/fileBrowser/dragAndDrop.js +115 -0
- package/lib/fileBrowser/fileBrowser.d.ts +2 -0
- package/lib/fileBrowser/fileBrowser.js +151 -197
- package/lib/fileBrowser/index.js +39 -6
- package/lib/fileBrowser/widget.d.ts +44 -5
- package/lib/fileBrowser/widget.js +153 -14
- package/lib/index.js +2 -0
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +15 -2
- package/lib/launcher/icons.js +15 -4
- package/lib/launcher/schemaDefaults.d.ts +2 -9
- package/lib/launcher/schemaDefaults.js +37 -13
- package/lib/sidebar/index.d.ts +7 -13
- package/lib/sidebar/index.js +80 -46
- package/lib/terminals/index.d.ts +5 -3
- package/lib/terminals/index.js +17 -6
- package/lib/terminals/widget.d.ts +29 -25
- package/lib/terminals/widget.js +150 -65
- package/package.json +1 -1
- package/schema/launcher.json +2 -2
- package/schema/sidebar.json +47 -7
- package/src/customPanel/index.ts +104 -0
- package/src/fileBrowser/commands.ts +14 -1
- package/src/fileBrowser/contents.ts +30 -0
- package/src/fileBrowser/dragAndDrop.ts +178 -0
- package/src/fileBrowser/fileBrowser.tsx +183 -225
- package/src/fileBrowser/index.ts +41 -6
- package/src/fileBrowser/widget.tsx +195 -14
- package/src/index.ts +2 -0
- package/src/launcher/agents.ts +16 -3
- package/src/launcher/icons.ts +16 -4
- package/src/launcher/schemaDefaults.ts +43 -13
- package/src/sidebar/index.ts +95 -52
- package/src/terminals/index.ts +24 -6
- package/src/terminals/widget.tsx +186 -87
- package/style/base.css +22 -34
- package/style/customPanel.css +9 -0
- package/style/index.js +1 -0
- package/style/terminals.css +19 -77
package/src/sidebar/index.ts
CHANGED
|
@@ -10,21 +10,46 @@ import type { Widget } from '@lumino/widgets';
|
|
|
10
10
|
const PLUGIN_ID = 'xtralab:sidebar';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `running-extension` adds the running panel at rank 200 — and we reuse
|
|
17
|
-
* them when re-adding so the tab lands back in its usual position.
|
|
13
|
+
* `rank` is the fallback used when re-adding a hidden tab; the shipped
|
|
14
|
+
* `layout` setting normally wins because `LabShell.add` merges its
|
|
15
|
+
* per-widget options over the ones passed in.
|
|
18
16
|
*/
|
|
19
17
|
interface ITarget {
|
|
20
18
|
id: string;
|
|
21
19
|
rank: number;
|
|
22
|
-
settingKey:
|
|
20
|
+
settingKey:
|
|
21
|
+
| 'showTerminals'
|
|
22
|
+
| 'showFileBrowser'
|
|
23
|
+
| 'showGitPanel'
|
|
24
|
+
| 'showDefaultFileBrowser'
|
|
25
|
+
| 'showRunningSessions'
|
|
26
|
+
| 'showSearchReplace';
|
|
23
27
|
command: string;
|
|
24
28
|
label: (trans: ReturnType<ITranslator['load']>) => string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
const TARGETS: ITarget[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 'xtralab-running-terminals',
|
|
34
|
+
rank: 1,
|
|
35
|
+
settingKey: 'showTerminals',
|
|
36
|
+
command: 'xtralab:toggle-terminals',
|
|
37
|
+
label: trans => trans.__('Terminals')
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'xtralab:file-browser',
|
|
41
|
+
rank: 2,
|
|
42
|
+
settingKey: 'showFileBrowser',
|
|
43
|
+
command: 'xtralab:toggle-file-browser',
|
|
44
|
+
label: trans => trans.__('File Browser')
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'jp-git-sessions',
|
|
48
|
+
rank: 200,
|
|
49
|
+
settingKey: 'showGitPanel',
|
|
50
|
+
command: 'xtralab:toggle-git-panel',
|
|
51
|
+
label: trans => trans.__('Git')
|
|
52
|
+
},
|
|
28
53
|
{
|
|
29
54
|
id: 'filebrowser',
|
|
30
55
|
rank: 100,
|
|
@@ -38,28 +63,29 @@ const TARGETS: ITarget[] = [
|
|
|
38
63
|
settingKey: 'showRunningSessions',
|
|
39
64
|
command: 'xtralab:toggle-running-sessions',
|
|
40
65
|
label: trans => trans.__('Running Terminals and Kernels')
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'jp-search-replace',
|
|
69
|
+
rank: 900,
|
|
70
|
+
settingKey: 'showSearchReplace',
|
|
71
|
+
command: 'xtralab:toggle-search-replace',
|
|
72
|
+
label: trans => trans.__('Search and Replace')
|
|
41
73
|
}
|
|
42
74
|
];
|
|
43
75
|
|
|
44
76
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* The menu placement is contributed declaratively in
|
|
53
|
-
* `schema/sidebar.json`, so this plugin only registers commands and
|
|
54
|
-
* applies state — it does not touch `IMainMenu` directly.
|
|
55
|
-
*
|
|
56
|
-
* The preference is persisted to `xtralab:sidebar` settings so the choice
|
|
57
|
-
* survives reloads.
|
|
77
|
+
* Toggles individual sidebar tabs from View > Appearance > Sidebars,
|
|
78
|
+
* whichever side a tab currently lives on. Hiding detaches the widget
|
|
79
|
+
* (`widget.parent = null`) while keeping the instance for a later
|
|
80
|
+
* re-add; the preference persists in this plugin's settings. Which side
|
|
81
|
+
* a hidden tab came from is only tracked in-memory, so after a reload a
|
|
82
|
+
* re-shown tab lands on the left. The menu placement is contributed
|
|
83
|
+
* declaratively in `schema/sidebar.json`.
|
|
58
84
|
*/
|
|
59
85
|
const plugin: JupyterFrontEndPlugin<void> = {
|
|
60
86
|
id: PLUGIN_ID,
|
|
61
87
|
description:
|
|
62
|
-
'Toggle visibility of
|
|
88
|
+
'Toggle visibility of individual sidebar tabs from the View menu.',
|
|
63
89
|
autoStart: true,
|
|
64
90
|
requires: [ILabShell],
|
|
65
91
|
optional: [ISettingRegistry, ITranslator],
|
|
@@ -72,31 +98,40 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
72
98
|
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
73
99
|
const { commands } = app;
|
|
74
100
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// reachable through `labShell.widgets('left')` once removed, so a
|
|
78
|
-
// missing cache entry would leave us unable to bring the tab back.
|
|
101
|
+
// Detached widgets are unreachable via `labShell.widgets(...)`; keep
|
|
102
|
+
// instances here so hidden tabs can be re-added.
|
|
79
103
|
const widgetCache = new Map<string, Widget>();
|
|
80
104
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
// Side each hidden widget was removed from, so re-showing restores it
|
|
106
|
+
// there.
|
|
107
|
+
const hiddenFrom = new Map<string, 'left' | 'right'>();
|
|
108
|
+
|
|
109
|
+
const findInSidebars = (
|
|
110
|
+
id: string
|
|
111
|
+
): { widget: Widget; area: 'left' | 'right' } | null => {
|
|
112
|
+
for (const area of ['left', 'right'] as const) {
|
|
113
|
+
for (const widget of labShell.widgets(area)) {
|
|
114
|
+
if (widget.id === id) {
|
|
115
|
+
return { widget, area };
|
|
116
|
+
}
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
return null;
|
|
88
120
|
};
|
|
89
121
|
|
|
122
|
+
const locate = (id: string): 'left' | 'right' | null =>
|
|
123
|
+
findInSidebars(id)?.area ?? null;
|
|
124
|
+
|
|
90
125
|
const captureWidget = (id: string): Widget | null => {
|
|
91
126
|
const cached = widgetCache.get(id);
|
|
92
127
|
if (cached && !cached.isDisposed) {
|
|
93
128
|
return cached;
|
|
94
129
|
}
|
|
95
|
-
const found =
|
|
130
|
+
const found = findInSidebars(id);
|
|
96
131
|
if (found) {
|
|
97
|
-
widgetCache.set(id, found);
|
|
132
|
+
widgetCache.set(id, found.widget);
|
|
98
133
|
}
|
|
99
|
-
return found;
|
|
134
|
+
return found?.widget ?? null;
|
|
100
135
|
};
|
|
101
136
|
|
|
102
137
|
let settings: ISettingRegistry.ISettings | null = null;
|
|
@@ -112,19 +147,24 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
112
147
|
const apply = (target: ITarget): void => {
|
|
113
148
|
const widget = captureWidget(target.id);
|
|
114
149
|
if (!widget) {
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
// Not seen yet (extension disabled or not added) — nothing to do.
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const area = locate(target.id);
|
|
154
|
+
if (!area && widget.parent !== null) {
|
|
155
|
+
// Attached outside the sidebars — leave it alone.
|
|
118
156
|
return;
|
|
119
157
|
}
|
|
120
|
-
const present = findInSidebar(target.id) !== null;
|
|
121
158
|
const wantPresent = readPreference(target);
|
|
122
|
-
if (wantPresent && !
|
|
123
|
-
labShell.add(widget, 'left', {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
159
|
+
if (wantPresent && !area) {
|
|
160
|
+
labShell.add(widget, hiddenFrom.get(target.id) ?? 'left', {
|
|
161
|
+
rank: target.rank
|
|
162
|
+
});
|
|
163
|
+
hiddenFrom.delete(target.id);
|
|
164
|
+
} else if (!wantPresent && area) {
|
|
165
|
+
hiddenFrom.set(target.id, area);
|
|
166
|
+
// Detaching makes the SideBarHandler remove the tab while the
|
|
167
|
+
// widget instance stays alive for a later re-add.
|
|
128
168
|
widget.parent = null;
|
|
129
169
|
}
|
|
130
170
|
};
|
|
@@ -139,14 +179,21 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
139
179
|
commands.addCommand(target.command, {
|
|
140
180
|
label: target.label(trans),
|
|
141
181
|
isToggled: () => readPreference(target),
|
|
182
|
+
isVisible: () => {
|
|
183
|
+
// List only tabs this plugin can manage: in a sidebar, or
|
|
184
|
+
// hidden by this plugin (`parent === null`).
|
|
185
|
+
const widget = captureWidget(target.id);
|
|
186
|
+
return (
|
|
187
|
+
widget !== null &&
|
|
188
|
+
(widget.parent === null || locate(target.id) !== null)
|
|
189
|
+
);
|
|
190
|
+
},
|
|
142
191
|
execute: async () => {
|
|
143
192
|
const next = !readPreference(target);
|
|
144
193
|
if (settings) {
|
|
145
194
|
try {
|
|
146
195
|
await settings.set(target.settingKey, next);
|
|
147
|
-
// `settings.changed`
|
|
148
|
-
// command-state refresh. Returning here keeps the toggle's
|
|
149
|
-
// single source of truth in the settings registry.
|
|
196
|
+
// `settings.changed` drives `apply` and the state refresh.
|
|
150
197
|
return;
|
|
151
198
|
} catch (reason) {
|
|
152
199
|
console.error(
|
|
@@ -155,9 +202,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
155
202
|
);
|
|
156
203
|
}
|
|
157
204
|
}
|
|
158
|
-
// No settings registry
|
|
159
|
-
//
|
|
160
|
-
// works for the current session.
|
|
205
|
+
// No settings registry (or the write failed): apply in-memory
|
|
206
|
+
// for the current session only.
|
|
161
207
|
apply(target);
|
|
162
208
|
commands.notifyCommandChanged(target.command);
|
|
163
209
|
}
|
|
@@ -179,11 +225,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
179
225
|
}
|
|
180
226
|
|
|
181
227
|
void app.restored.then(() => {
|
|
182
|
-
// Capture
|
|
183
|
-
// recoverable
|
|
184
|
-
// the targets that need to stay visible — by capturing every target
|
|
185
|
-
// here we keep the cache populated for both directions of the
|
|
186
|
-
// toggle.
|
|
228
|
+
// Capture every widget before applying, so tabs hidden on startup
|
|
229
|
+
// stay recoverable.
|
|
187
230
|
for (const target of TARGETS) {
|
|
188
231
|
captureWidget(target.id);
|
|
189
232
|
}
|
package/src/terminals/index.ts
CHANGED
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
JupyterFrontEnd,
|
|
4
4
|
JupyterFrontEndPlugin
|
|
5
5
|
} from '@jupyterlab/application';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
IMovableSectionRegistry,
|
|
9
|
+
showDialog
|
|
10
|
+
} from '@jupyterlab/apputils';
|
|
7
11
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
8
12
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
9
13
|
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
@@ -36,9 +40,11 @@ const PLUGIN_ID = 'xtralab:terminals';
|
|
|
36
40
|
*
|
|
37
41
|
* Clicking a row activates the existing tab if one is open, or reopens
|
|
38
42
|
* the session in a fresh terminal widget (`terminal:open`). The inline
|
|
39
|
-
* `×` button shuts the session down on the server. The
|
|
40
|
-
* `+` button and a stop button (the
|
|
41
|
-
* once, after a confirmation).
|
|
43
|
+
* `×` button shuts the session down on the server. The Terminals
|
|
44
|
+
* section's title toolbar carries a `+` button and a stop button (the
|
|
45
|
+
* latter shuts every session down at once, after a confirmation).
|
|
46
|
+
*
|
|
47
|
+
* The list is one movable section, and other panels' sections can move in.
|
|
42
48
|
*
|
|
43
49
|
* The `+` button drops down a menu — built from the launcher's shared
|
|
44
50
|
* `IAgentRegistry` — listing each available agent (Claude, Codex, …)
|
|
@@ -65,7 +71,8 @@ const plugin: JupyterFrontEndPlugin<IAgentTerminals> = {
|
|
|
65
71
|
IAgentRegistry,
|
|
66
72
|
IEditorRegistry,
|
|
67
73
|
IAgentSessions,
|
|
68
|
-
ISettingRegistry
|
|
74
|
+
ISettingRegistry,
|
|
75
|
+
IMovableSectionRegistry
|
|
69
76
|
],
|
|
70
77
|
activate: (
|
|
71
78
|
app: JupyterFrontEnd,
|
|
@@ -75,7 +82,8 @@ const plugin: JupyterFrontEndPlugin<IAgentTerminals> = {
|
|
|
75
82
|
agentRegistry: IAgentRegistry | null,
|
|
76
83
|
editorRegistry: IEditorRegistry | null,
|
|
77
84
|
agentSessions: IAgentSessions | null,
|
|
78
|
-
settingRegistry: ISettingRegistry | null
|
|
85
|
+
settingRegistry: ISettingRegistry | null,
|
|
86
|
+
movableSections: IMovableSectionRegistry | null
|
|
79
87
|
): IAgentTerminals => {
|
|
80
88
|
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
81
89
|
|
|
@@ -262,6 +270,16 @@ const plugin: JupyterFrontEndPlugin<IAgentTerminals> = {
|
|
|
262
270
|
restorer.add(panel, panel.id);
|
|
263
271
|
}
|
|
264
272
|
|
|
273
|
+
void app.restored.then(() => {
|
|
274
|
+
panel.expandOwnSection();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (movableSections) {
|
|
278
|
+
movableSections.registerSource(PLUGIN_ID, trans.__('Terminals'), panel);
|
|
279
|
+
movableSections.registerTarget(PLUGIN_ID, trans.__('Terminals'), panel);
|
|
280
|
+
panel.announceSections();
|
|
281
|
+
}
|
|
282
|
+
|
|
265
283
|
// The latest-activity line under each agent row is on by default; honour
|
|
266
284
|
// the `showAgentActivity` setting so it can be turned off. Read on load and
|
|
267
285
|
// re-applied on every change.
|
package/src/terminals/widget.tsx
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IMovableSectionDestination,
|
|
3
|
+
IMovableSectionSource,
|
|
4
|
+
ISectionEntry
|
|
5
|
+
} from '@jupyterlab/apputils';
|
|
1
6
|
import {
|
|
2
7
|
type TranslationBundle,
|
|
3
8
|
nullTranslator
|
|
@@ -6,11 +11,16 @@ import {
|
|
|
6
11
|
addIcon,
|
|
7
12
|
closeIcon,
|
|
8
13
|
LabIcon,
|
|
14
|
+
PanelWithToolbar,
|
|
9
15
|
ReactWidget,
|
|
16
|
+
SidePanel,
|
|
10
17
|
stopIcon,
|
|
11
18
|
terminalIcon,
|
|
19
|
+
ToolbarButton,
|
|
12
20
|
UseSignal
|
|
13
21
|
} from '@jupyterlab/ui-components';
|
|
22
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
23
|
+
import { AccordionPanel, Widget } from '@lumino/widgets';
|
|
14
24
|
import * as React from 'react';
|
|
15
25
|
|
|
16
26
|
import { SessionRegistry } from './model';
|
|
@@ -22,37 +32,134 @@ import { SessionRegistry } from './model';
|
|
|
22
32
|
const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* launcher *starts* sessions, this panel surfaces the ones already
|
|
28
|
-
* running so the user can jump back to a backgrounded agent — including
|
|
29
|
-
* sessions whose tab has been closed but which are still alive on the
|
|
30
|
-
* server. Each row is labelled with the session's title and, for a session
|
|
31
|
-
* running a coding agent, a smaller line below it showing the agent's latest
|
|
32
|
-
* line of output (from {@link SessionRegistry.activityFor}).
|
|
33
|
-
*
|
|
34
|
-
* Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
|
|
35
|
-
* whenever the registry emits `stateChanged`, so it tracks
|
|
36
|
-
* `runningChanged` and the agent-title cache without any local state.
|
|
37
|
-
* Click behavior (activate vs. reopen, shutdown, new terminal) is
|
|
38
|
-
* delegated to plugin-supplied callbacks so the widget never imports
|
|
39
|
-
* `app`.
|
|
35
|
+
* Id of the accordion section holding the terminals list. Persisted by the
|
|
36
|
+
* movable-sections plugin, so it must stay stable across releases.
|
|
40
37
|
*/
|
|
41
|
-
|
|
38
|
+
const TERMINALS_SECTION_ID = 'xtralab-terminals-section';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Left-sidebar panel listing every running terminal session, including the
|
|
42
|
+
* sessions whose tab is closed but which are still alive on the server. The
|
|
43
|
+
* list is a single movable "Terminals" section.
|
|
44
|
+
*/
|
|
45
|
+
export class RunningTerminals
|
|
46
|
+
extends SidePanel
|
|
47
|
+
implements IMovableSectionSource, IMovableSectionDestination
|
|
48
|
+
{
|
|
42
49
|
constructor(options: RunningTerminals.IOptions) {
|
|
43
50
|
super();
|
|
44
51
|
this._registry = options.registry;
|
|
45
|
-
|
|
46
|
-
this._iconForCommand = options.iconForCommand;
|
|
47
|
-
this._onActivate = options.onActivate;
|
|
48
|
-
this._onShutdown = options.onShutdown;
|
|
49
|
-
this._onShutdownAll = options.onShutdownAll;
|
|
50
|
-
this._onCreate = options.onCreate;
|
|
52
|
+
const trans = options.trans ?? nullTranslator.load('jupyterlab');
|
|
51
53
|
|
|
52
54
|
this.id = RUNNING_TERMINALS_ID;
|
|
53
55
|
this.title.icon = terminalIcon;
|
|
54
|
-
this.title.caption =
|
|
56
|
+
this.title.caption = trans.__('Running Terminals');
|
|
55
57
|
this.addClass('jp-xtralab-Terminals');
|
|
58
|
+
|
|
59
|
+
const section = (this._section = new PanelWithToolbar());
|
|
60
|
+
section.id = TERMINALS_SECTION_ID;
|
|
61
|
+
section.title.label = trans.__('Terminals');
|
|
62
|
+
section.addClass('jp-xtralab-Terminals-section');
|
|
63
|
+
|
|
64
|
+
const shutdownAll = new ToolbarButton({
|
|
65
|
+
icon: stopIcon,
|
|
66
|
+
onClick: options.onShutdownAll,
|
|
67
|
+
tooltip: trans.__('Shut Down All Terminals'),
|
|
68
|
+
enabled: this._registry.sessionNames().length > 0
|
|
69
|
+
});
|
|
70
|
+
this._registry.stateChanged.connect(() => {
|
|
71
|
+
shutdownAll.enabled = this._registry.sessionNames().length > 0;
|
|
72
|
+
});
|
|
73
|
+
section.toolbar.addItem('shutdown-all', shutdownAll);
|
|
74
|
+
|
|
75
|
+
const newTerminal = new ToolbarButton({
|
|
76
|
+
icon: addIcon,
|
|
77
|
+
onClick: () => {
|
|
78
|
+
// Anchor whatever the plugin shows (usually an agent menu) to the
|
|
79
|
+
// button's bottom-left, in the viewport coordinates `Menu.open` wants.
|
|
80
|
+
const rect = newTerminal.node.getBoundingClientRect();
|
|
81
|
+
options.onCreate({ x: rect.left, y: rect.bottom });
|
|
82
|
+
},
|
|
83
|
+
tooltip: trans.__('New Terminal')
|
|
84
|
+
});
|
|
85
|
+
section.toolbar.addItem('new-terminal', newTerminal);
|
|
86
|
+
|
|
87
|
+
section.addWidget(
|
|
88
|
+
new TerminalsListing({
|
|
89
|
+
registry: this._registry,
|
|
90
|
+
trans,
|
|
91
|
+
iconForCommand: options.iconForCommand,
|
|
92
|
+
onActivate: options.onActivate,
|
|
93
|
+
onShutdown: options.onShutdown
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
this.addWidget(section);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get accordionPanel(): AccordionPanel {
|
|
101
|
+
return this.content as AccordionPanel;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get sectionAdded(): ISignal<this, ISectionEntry> {
|
|
105
|
+
return this._sectionAdded;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get sections(): ReadonlyArray<Widget> {
|
|
109
|
+
return this.accordionPanel.widgets.filter(w => w !== this._section);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getSections(): ReadonlyArray<ISectionEntry> {
|
|
113
|
+
const entry = this._sectionEntry();
|
|
114
|
+
return entry ? [entry] : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
removeSectionById(sectionId: string): Widget | null {
|
|
118
|
+
if (
|
|
119
|
+
sectionId !== TERMINALS_SECTION_ID ||
|
|
120
|
+
this._section.parent !== this.content
|
|
121
|
+
) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
this._section.parent = null;
|
|
125
|
+
return this._section;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
reinsertSection(widget: Widget): void {
|
|
129
|
+
this.addWidget(widget);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
addSection(widget: Widget): void {
|
|
133
|
+
this.addWidget(widget);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
removeSectionWidget(widget: Widget): void {
|
|
137
|
+
if (widget.parent === this.content) {
|
|
138
|
+
widget.parent = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The layout restorer stores a section as collapsed whenever its panel is off
|
|
144
|
+
* screen at save time, so it can come back collapsed without the user asking.
|
|
145
|
+
*/
|
|
146
|
+
expandOwnSection(): void {
|
|
147
|
+
const accordion = this.accordionPanel;
|
|
148
|
+
const index = Array.from(accordion.widgets).indexOf(this._section);
|
|
149
|
+
if (index >= 0) {
|
|
150
|
+
accordion.expand(index);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The move plugin reconciles persisted moves only against sections announced
|
|
156
|
+
* after `registerSource`, and this panel builds its section in the constructor.
|
|
157
|
+
*/
|
|
158
|
+
announceSections(): void {
|
|
159
|
+
const entry = this._sectionEntry();
|
|
160
|
+
if (entry) {
|
|
161
|
+
this._sectionAdded.emit(entry);
|
|
162
|
+
}
|
|
56
163
|
}
|
|
57
164
|
|
|
58
165
|
dispose(): void {
|
|
@@ -65,31 +172,23 @@ export class RunningTerminals extends ReactWidget {
|
|
|
65
172
|
super.dispose();
|
|
66
173
|
}
|
|
67
174
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/>
|
|
81
|
-
)}
|
|
82
|
-
</UseSignal>
|
|
83
|
-
);
|
|
175
|
+
private _sectionEntry(): ISectionEntry | null {
|
|
176
|
+
const accordion = this.accordionPanel;
|
|
177
|
+
const index = Array.from(accordion.widgets).indexOf(this._section);
|
|
178
|
+
const titleNode = accordion.titles[index] as HTMLElement | undefined;
|
|
179
|
+
if (index < 0 || !titleNode) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
id: TERMINALS_SECTION_ID,
|
|
184
|
+
titleNode,
|
|
185
|
+
widget: this._section
|
|
186
|
+
};
|
|
84
187
|
}
|
|
85
188
|
|
|
86
189
|
private _registry: SessionRegistry;
|
|
87
|
-
private
|
|
88
|
-
private
|
|
89
|
-
private _onActivate: (sessionName: string) => void;
|
|
90
|
-
private _onShutdown: (sessionName: string) => void;
|
|
91
|
-
private _onShutdownAll: () => void;
|
|
92
|
-
private _onCreate: (anchor: { x: number; y: number }) => void;
|
|
190
|
+
private _section: PanelWithToolbar;
|
|
191
|
+
private _sectionAdded = new Signal<this, ISectionEntry>(this);
|
|
93
192
|
}
|
|
94
193
|
|
|
95
194
|
export namespace RunningTerminals {
|
|
@@ -128,24 +227,55 @@ export namespace RunningTerminals {
|
|
|
128
227
|
}
|
|
129
228
|
}
|
|
130
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The body of the Terminals section.
|
|
232
|
+
*/
|
|
233
|
+
class TerminalsListing extends ReactWidget {
|
|
234
|
+
constructor(options: {
|
|
235
|
+
registry: SessionRegistry;
|
|
236
|
+
trans: TranslationBundle;
|
|
237
|
+
iconForCommand: (command: string | null) => LabIcon;
|
|
238
|
+
onActivate: (sessionName: string) => void;
|
|
239
|
+
onShutdown: (sessionName: string) => void;
|
|
240
|
+
}) {
|
|
241
|
+
super();
|
|
242
|
+
this._options = options;
|
|
243
|
+
this.addClass('jp-xtralab-Terminals-listing');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
protected render(): React.ReactElement {
|
|
247
|
+
return (
|
|
248
|
+
<UseSignal signal={this._options.registry.stateChanged}>
|
|
249
|
+
{() => (
|
|
250
|
+
<RunningTerminalsComponent
|
|
251
|
+
registry={this._options.registry}
|
|
252
|
+
trans={this._options.trans}
|
|
253
|
+
iconForCommand={this._options.iconForCommand}
|
|
254
|
+
onActivate={this._options.onActivate}
|
|
255
|
+
onShutdown={this._options.onShutdown}
|
|
256
|
+
/>
|
|
257
|
+
)}
|
|
258
|
+
</UseSignal>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private _options: {
|
|
263
|
+
registry: SessionRegistry;
|
|
264
|
+
trans: TranslationBundle;
|
|
265
|
+
iconForCommand: (command: string | null) => LabIcon;
|
|
266
|
+
onActivate: (sessionName: string) => void;
|
|
267
|
+
onShutdown: (sessionName: string) => void;
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
131
271
|
function RunningTerminalsComponent(props: {
|
|
132
272
|
registry: SessionRegistry;
|
|
133
273
|
trans: TranslationBundle;
|
|
134
274
|
iconForCommand: (command: string | null) => LabIcon;
|
|
135
275
|
onActivate: (sessionName: string) => void;
|
|
136
276
|
onShutdown: (sessionName: string) => void;
|
|
137
|
-
onShutdownAll: () => void;
|
|
138
|
-
onCreate: (anchor: { x: number; y: number }) => void;
|
|
139
277
|
}): React.ReactElement {
|
|
140
|
-
const {
|
|
141
|
-
registry,
|
|
142
|
-
trans,
|
|
143
|
-
iconForCommand,
|
|
144
|
-
onActivate,
|
|
145
|
-
onShutdown,
|
|
146
|
-
onShutdownAll,
|
|
147
|
-
onCreate
|
|
148
|
-
} = props;
|
|
278
|
+
const { registry, trans, iconForCommand, onActivate, onShutdown } = props;
|
|
149
279
|
const names = registry.sessionNames();
|
|
150
280
|
// The session whose terminal is the current widget in the main area, so its
|
|
151
281
|
// row can be highlighted. `null` when the current tab is a notebook or any
|
|
@@ -154,37 +284,6 @@ function RunningTerminalsComponent(props: {
|
|
|
154
284
|
|
|
155
285
|
return (
|
|
156
286
|
<div className="jp-xtralab-Terminals-body">
|
|
157
|
-
<div className="jp-xtralab-Terminals-header">
|
|
158
|
-
<h2 className="jp-xtralab-Terminals-title">{trans.__('Terminals')}</h2>
|
|
159
|
-
<div className="jp-xtralab-Terminals-actions">
|
|
160
|
-
<button
|
|
161
|
-
type="button"
|
|
162
|
-
className="jp-xtralab-Terminals-action jp-xtralab-Terminals-shutdown-all"
|
|
163
|
-
onClick={onShutdownAll}
|
|
164
|
-
disabled={names.length === 0}
|
|
165
|
-
title={trans.__('Shut Down All Terminals')}
|
|
166
|
-
aria-label={trans.__('Shut Down All Terminals')}
|
|
167
|
-
>
|
|
168
|
-
<stopIcon.react tag="span" verticalAlign="middle" />
|
|
169
|
-
</button>
|
|
170
|
-
<button
|
|
171
|
-
type="button"
|
|
172
|
-
className="jp-xtralab-Terminals-action jp-xtralab-Terminals-new"
|
|
173
|
-
onClick={event => {
|
|
174
|
-
// Anchor whatever the plugin shows (usually an agent menu) to
|
|
175
|
-
// the button's bottom-left, in viewport coordinates — what
|
|
176
|
-
// `Menu.open(x, y)` expects.
|
|
177
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
178
|
-
onCreate({ x: rect.left, y: rect.bottom });
|
|
179
|
-
}}
|
|
180
|
-
title={trans.__('New Terminal')}
|
|
181
|
-
aria-label={trans.__('New Terminal')}
|
|
182
|
-
aria-haspopup="menu"
|
|
183
|
-
>
|
|
184
|
-
<addIcon.react tag="span" verticalAlign="middle" />
|
|
185
|
-
</button>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
287
|
{names.length === 0 ? (
|
|
189
288
|
<p className="jp-xtralab-Terminals-empty">
|
|
190
289
|
{trans.__('No running terminals.')}
|