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.
Files changed (45) hide show
  1. package/lib/customPanel/index.d.ts +7 -0
  2. package/lib/customPanel/index.js +74 -0
  3. package/lib/fileBrowser/commands.js +13 -1
  4. package/lib/fileBrowser/contents.d.ts +11 -0
  5. package/lib/fileBrowser/contents.js +28 -0
  6. package/lib/fileBrowser/dragAndDrop.d.ts +66 -0
  7. package/lib/fileBrowser/dragAndDrop.js +115 -0
  8. package/lib/fileBrowser/fileBrowser.d.ts +2 -0
  9. package/lib/fileBrowser/fileBrowser.js +151 -197
  10. package/lib/fileBrowser/index.js +39 -6
  11. package/lib/fileBrowser/widget.d.ts +44 -5
  12. package/lib/fileBrowser/widget.js +153 -14
  13. package/lib/index.js +2 -0
  14. package/lib/launcher/agents.d.ts +1 -1
  15. package/lib/launcher/agents.js +15 -2
  16. package/lib/launcher/icons.js +15 -4
  17. package/lib/launcher/schemaDefaults.d.ts +2 -9
  18. package/lib/launcher/schemaDefaults.js +37 -13
  19. package/lib/sidebar/index.d.ts +7 -13
  20. package/lib/sidebar/index.js +80 -46
  21. package/lib/terminals/index.d.ts +5 -3
  22. package/lib/terminals/index.js +17 -6
  23. package/lib/terminals/widget.d.ts +29 -25
  24. package/lib/terminals/widget.js +150 -65
  25. package/package.json +1 -1
  26. package/schema/launcher.json +2 -2
  27. package/schema/sidebar.json +47 -7
  28. package/src/customPanel/index.ts +104 -0
  29. package/src/fileBrowser/commands.ts +14 -1
  30. package/src/fileBrowser/contents.ts +30 -0
  31. package/src/fileBrowser/dragAndDrop.ts +178 -0
  32. package/src/fileBrowser/fileBrowser.tsx +183 -225
  33. package/src/fileBrowser/index.ts +41 -6
  34. package/src/fileBrowser/widget.tsx +195 -14
  35. package/src/index.ts +2 -0
  36. package/src/launcher/agents.ts +16 -3
  37. package/src/launcher/icons.ts +16 -4
  38. package/src/launcher/schemaDefaults.ts +43 -13
  39. package/src/sidebar/index.ts +95 -52
  40. package/src/terminals/index.ts +24 -6
  41. package/src/terminals/widget.tsx +186 -87
  42. package/style/base.css +22 -34
  43. package/style/customPanel.css +9 -0
  44. package/style/index.js +1 -0
  45. package/style/terminals.css +19 -77
@@ -10,21 +10,46 @@ import type { Widget } from '@lumino/widgets';
10
10
  const PLUGIN_ID = 'xtralab:sidebar';
11
11
 
12
12
  /**
13
- * Each entry pairs an upstream sidebar widget id with the rank it was
14
- * originally added at. The ranks come from the upstream extensions —
15
- * `filebrowser-extension` adds the default browser at rank 100 and
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: 'showDefaultFileBrowser' | 'showRunningSessions';
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
- * Lets the user toggle individual left-sidebar tabs from
46
- * View > Appearance > Left Sidebar. Removing a widget from a `LabShell`
47
- * side area is done by setting `widget.parent = null`: the StackedPanel
48
- * emits `widgetRemoved`, which the SideBarHandler responds to by removing
49
- * the matching tab. The widget instance is preserved, so we can re-add it
50
- * later via `labShell.add` with its original rank.
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 selected sidebar tabs (default file browser, running sessions) from the View menu.',
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
- // Cache widget instances so we can re-add them after `widget.parent =
76
- // null` has detached them from the sidebar. The widget is no longer
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
- const findInSidebar = (id: string): Widget | null => {
82
- for (const w of labShell.widgets('left')) {
83
- if (w.id === id) {
84
- return w;
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 = findInSidebar(id);
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
- // The upstream widget hasn't been added yet (or the extension is
116
- // disabled). Nothing to do — the next time `apply` runs and finds
117
- // it, the preference will take effect.
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 && !present) {
123
- labShell.add(widget, 'left', { rank: target.rank });
124
- } else if (!wantPresent && present) {
125
- // Setting `parent = null` triggers SideBarHandler's
126
- // `_onWidgetRemoved`, which strips the tab while leaving the
127
- // widget instance alive for a later re-add.
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` will fire and drive `apply` + the
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 available (or the write failed): fall
159
- // back to applying the change in-memory so the toggle still
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 references first so a hidden-on-startup widget is still
183
- // recoverable later. `apply` would also do the lookup, but only on
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
  }
@@ -3,7 +3,11 @@ import {
3
3
  JupyterFrontEnd,
4
4
  JupyterFrontEndPlugin
5
5
  } from '@jupyterlab/application';
6
- import { Dialog, showDialog } from '@jupyterlab/apputils';
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 header carries a
40
- * `+` button and a stop button (the latter shuts every session down at
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.
@@ -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
- * Left-sidebar panel listing every running terminal session. It is the
26
- * sidebar counterpart of the launcher's terminal tiles: where the
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
- export class RunningTerminals extends ReactWidget {
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
- this._trans = options.trans ?? nullTranslator.load('jupyterlab');
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 = this._trans.__('Running Terminals');
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
- protected render(): React.ReactElement {
69
- return (
70
- <UseSignal signal={this._registry.stateChanged}>
71
- {() => (
72
- <RunningTerminalsComponent
73
- registry={this._registry}
74
- trans={this._trans}
75
- iconForCommand={this._iconForCommand}
76
- onActivate={this._onActivate}
77
- onShutdown={this._onShutdown}
78
- onShutdownAll={this._onShutdownAll}
79
- onCreate={this._onCreate}
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 _trans: TranslationBundle;
88
- private _iconForCommand: (command: string | null) => LabIcon;
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.')}