xtralab 0.14.0 → 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 (52) 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 +4 -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/menuBar/icons.d.ts +6 -0
  20. package/lib/menuBar/icons.js +13 -0
  21. package/lib/menuBar/index.d.ts +15 -0
  22. package/lib/menuBar/index.js +164 -0
  23. package/lib/sidebar/index.d.ts +7 -13
  24. package/lib/sidebar/index.js +80 -46
  25. package/lib/terminals/index.d.ts +5 -3
  26. package/lib/terminals/index.js +17 -6
  27. package/lib/terminals/widget.d.ts +29 -25
  28. package/lib/terminals/widget.js +150 -65
  29. package/package.json +1 -1
  30. package/schema/launcher.json +2 -2
  31. package/schema/menu-bar.json +37 -0
  32. package/schema/sidebar.json +47 -7
  33. package/src/customPanel/index.ts +104 -0
  34. package/src/fileBrowser/commands.ts +14 -1
  35. package/src/fileBrowser/contents.ts +30 -0
  36. package/src/fileBrowser/dragAndDrop.ts +178 -0
  37. package/src/fileBrowser/fileBrowser.tsx +183 -225
  38. package/src/fileBrowser/index.ts +41 -6
  39. package/src/fileBrowser/widget.tsx +195 -14
  40. package/src/index.ts +4 -0
  41. package/src/launcher/agents.ts +16 -3
  42. package/src/launcher/icons.ts +16 -4
  43. package/src/launcher/schemaDefaults.ts +43 -13
  44. package/src/menuBar/icons.ts +14 -0
  45. package/src/menuBar/index.ts +195 -0
  46. package/src/sidebar/index.ts +95 -52
  47. package/src/terminals/index.ts +24 -6
  48. package/src/terminals/widget.tsx +186 -87
  49. package/style/base.css +22 -34
  50. package/style/customPanel.css +9 -0
  51. package/style/index.js +1 -0
  52. package/style/terminals.css +19 -77
@@ -3,6 +3,27 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
3
3
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
4
4
  const PLUGIN_ID = 'xtralab:sidebar';
5
5
  const TARGETS = [
6
+ {
7
+ id: 'xtralab-running-terminals',
8
+ rank: 1,
9
+ settingKey: 'showTerminals',
10
+ command: 'xtralab:toggle-terminals',
11
+ label: trans => trans.__('Terminals')
12
+ },
13
+ {
14
+ id: 'xtralab:file-browser',
15
+ rank: 2,
16
+ settingKey: 'showFileBrowser',
17
+ command: 'xtralab:toggle-file-browser',
18
+ label: trans => trans.__('File Browser')
19
+ },
20
+ {
21
+ id: 'jp-git-sessions',
22
+ rank: 200,
23
+ settingKey: 'showGitPanel',
24
+ command: 'xtralab:toggle-git-panel',
25
+ label: trans => trans.__('Git')
26
+ },
6
27
  {
7
28
  id: 'filebrowser',
8
29
  rank: 100,
@@ -16,55 +37,61 @@ const TARGETS = [
16
37
  settingKey: 'showRunningSessions',
17
38
  command: 'xtralab:toggle-running-sessions',
18
39
  label: trans => trans.__('Running Terminals and Kernels')
40
+ },
41
+ {
42
+ id: 'jp-search-replace',
43
+ rank: 900,
44
+ settingKey: 'showSearchReplace',
45
+ command: 'xtralab:toggle-search-replace',
46
+ label: trans => trans.__('Search and Replace')
19
47
  }
20
48
  ];
21
49
  /**
22
- * Lets the user toggle individual left-sidebar tabs from
23
- * View > Appearance > Left Sidebar. Removing a widget from a `LabShell`
24
- * side area is done by setting `widget.parent = null`: the StackedPanel
25
- * emits `widgetRemoved`, which the SideBarHandler responds to by removing
26
- * the matching tab. The widget instance is preserved, so we can re-add it
27
- * later via `labShell.add` with its original rank.
28
- *
29
- * The menu placement is contributed declaratively in
30
- * `schema/sidebar.json`, so this plugin only registers commands and
31
- * applies state — it does not touch `IMainMenu` directly.
32
- *
33
- * The preference is persisted to `xtralab:sidebar` settings so the choice
34
- * survives reloads.
50
+ * Toggles individual sidebar tabs from View > Appearance > Sidebars,
51
+ * whichever side a tab currently lives on. Hiding detaches the widget
52
+ * (`widget.parent = null`) while keeping the instance for a later
53
+ * re-add; the preference persists in this plugin's settings. Which side
54
+ * a hidden tab came from is only tracked in-memory, so after a reload a
55
+ * re-shown tab lands on the left. The menu placement is contributed
56
+ * declaratively in `schema/sidebar.json`.
35
57
  */
36
58
  const plugin = {
37
59
  id: PLUGIN_ID,
38
- description: 'Toggle visibility of selected sidebar tabs (default file browser, running sessions) from the View menu.',
60
+ description: 'Toggle visibility of individual sidebar tabs from the View menu.',
39
61
  autoStart: true,
40
62
  requires: [ILabShell],
41
63
  optional: [ISettingRegistry, ITranslator],
42
64
  activate: async (app, labShell, settingRegistry, translator) => {
43
65
  const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
44
66
  const { commands } = app;
45
- // Cache widget instances so we can re-add them after `widget.parent =
46
- // null` has detached them from the sidebar. The widget is no longer
47
- // reachable through `labShell.widgets('left')` once removed, so a
48
- // missing cache entry would leave us unable to bring the tab back.
67
+ // Detached widgets are unreachable via `labShell.widgets(...)`; keep
68
+ // instances here so hidden tabs can be re-added.
49
69
  const widgetCache = new Map();
50
- const findInSidebar = (id) => {
51
- for (const w of labShell.widgets('left')) {
52
- if (w.id === id) {
53
- return w;
70
+ // Side each hidden widget was removed from, so re-showing restores it
71
+ // there.
72
+ const hiddenFrom = new Map();
73
+ const findInSidebars = (id) => {
74
+ for (const area of ['left', 'right']) {
75
+ for (const widget of labShell.widgets(area)) {
76
+ if (widget.id === id) {
77
+ return { widget, area };
78
+ }
54
79
  }
55
80
  }
56
81
  return null;
57
82
  };
83
+ const locate = (id) => { var _a, _b; return (_b = (_a = findInSidebars(id)) === null || _a === void 0 ? void 0 : _a.area) !== null && _b !== void 0 ? _b : null; };
58
84
  const captureWidget = (id) => {
85
+ var _a;
59
86
  const cached = widgetCache.get(id);
60
87
  if (cached && !cached.isDisposed) {
61
88
  return cached;
62
89
  }
63
- const found = findInSidebar(id);
90
+ const found = findInSidebars(id);
64
91
  if (found) {
65
- widgetCache.set(id, found);
92
+ widgetCache.set(id, found.widget);
66
93
  }
67
- return found;
94
+ return (_a = found === null || found === void 0 ? void 0 : found.widget) !== null && _a !== void 0 ? _a : null;
68
95
  };
69
96
  let settings = null;
70
97
  const readPreference = (target) => {
@@ -75,22 +102,28 @@ const plugin = {
75
102
  return typeof value === 'boolean' ? value : true;
76
103
  };
77
104
  const apply = (target) => {
105
+ var _a;
78
106
  const widget = captureWidget(target.id);
79
107
  if (!widget) {
80
- // The upstream widget hasn't been added yet (or the extension is
81
- // disabled). Nothing to do — the next time `apply` runs and finds
82
- // it, the preference will take effect.
108
+ // Not seen yet (extension disabled or not added) — nothing to do.
109
+ return;
110
+ }
111
+ const area = locate(target.id);
112
+ if (!area && widget.parent !== null) {
113
+ // Attached outside the sidebars — leave it alone.
83
114
  return;
84
115
  }
85
- const present = findInSidebar(target.id) !== null;
86
116
  const wantPresent = readPreference(target);
87
- if (wantPresent && !present) {
88
- labShell.add(widget, 'left', { rank: target.rank });
117
+ if (wantPresent && !area) {
118
+ labShell.add(widget, (_a = hiddenFrom.get(target.id)) !== null && _a !== void 0 ? _a : 'left', {
119
+ rank: target.rank
120
+ });
121
+ hiddenFrom.delete(target.id);
89
122
  }
90
- else if (!wantPresent && present) {
91
- // Setting `parent = null` triggers SideBarHandler's
92
- // `_onWidgetRemoved`, which strips the tab while leaving the
93
- // widget instance alive for a later re-add.
123
+ else if (!wantPresent && area) {
124
+ hiddenFrom.set(target.id, area);
125
+ // Detaching makes the SideBarHandler remove the tab while the
126
+ // widget instance stays alive for a later re-add.
94
127
  widget.parent = null;
95
128
  }
96
129
  };
@@ -103,23 +136,27 @@ const plugin = {
103
136
  commands.addCommand(target.command, {
104
137
  label: target.label(trans),
105
138
  isToggled: () => readPreference(target),
139
+ isVisible: () => {
140
+ // List only tabs this plugin can manage: in a sidebar, or
141
+ // hidden by this plugin (`parent === null`).
142
+ const widget = captureWidget(target.id);
143
+ return (widget !== null &&
144
+ (widget.parent === null || locate(target.id) !== null));
145
+ },
106
146
  execute: async () => {
107
147
  const next = !readPreference(target);
108
148
  if (settings) {
109
149
  try {
110
150
  await settings.set(target.settingKey, next);
111
- // `settings.changed` will fire and drive `apply` + the
112
- // command-state refresh. Returning here keeps the toggle's
113
- // single source of truth in the settings registry.
151
+ // `settings.changed` drives `apply` and the state refresh.
114
152
  return;
115
153
  }
116
154
  catch (reason) {
117
155
  console.error(`xtralab: failed to persist ${target.settingKey}`, reason);
118
156
  }
119
157
  }
120
- // No settings registry available (or the write failed): fall
121
- // back to applying the change in-memory so the toggle still
122
- // works for the current session.
158
+ // No settings registry (or the write failed): apply in-memory
159
+ // for the current session only.
123
160
  apply(target);
124
161
  commands.notifyCommandChanged(target.command);
125
162
  }
@@ -140,11 +177,8 @@ const plugin = {
140
177
  }
141
178
  }
142
179
  void app.restored.then(() => {
143
- // Capture references first so a hidden-on-startup widget is still
144
- // recoverable later. `apply` would also do the lookup, but only on
145
- // the targets that need to stay visible — by capturing every target
146
- // here we keep the cache populated for both directions of the
147
- // toggle.
180
+ // Capture every widget before applying, so tabs hidden on startup
181
+ // stay recoverable.
148
182
  for (const target of TARGETS) {
149
183
  captureWidget(target.id);
150
184
  }
@@ -17,9 +17,11 @@ import { IAgentTerminals } from './tokens';
17
17
  *
18
18
  * Clicking a row activates the existing tab if one is open, or reopens
19
19
  * the session in a fresh terminal widget (`terminal:open`). The inline
20
- * `×` button shuts the session down on the server. The header carries a
21
- * `+` button and a stop button (the latter shuts every session down at
22
- * once, after a confirmation).
20
+ * `×` button shuts the session down on the server. The Terminals
21
+ * section's title toolbar carries a `+` button and a stop button (the
22
+ * latter shuts every session down at once, after a confirmation).
23
+ *
24
+ * The list is one movable section, and other panels' sections can move in.
23
25
  *
24
26
  * The `+` button drops down a menu — built from the launcher's shared
25
27
  * `IAgentRegistry` — listing each available agent (Claude, Codex, …)
@@ -1,5 +1,5 @@
1
1
  import { ILayoutRestorer } from '@jupyterlab/application';
2
- import { Dialog, showDialog } from '@jupyterlab/apputils';
2
+ import { Dialog, IMovableSectionRegistry, showDialog } from '@jupyterlab/apputils';
3
3
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
4
4
  import { ITerminalTracker } from '@jupyterlab/terminal';
5
5
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
@@ -29,9 +29,11 @@ const PLUGIN_ID = 'xtralab:terminals';
29
29
  *
30
30
  * Clicking a row activates the existing tab if one is open, or reopens
31
31
  * the session in a fresh terminal widget (`terminal:open`). The inline
32
- * `×` button shuts the session down on the server. The header carries a
33
- * `+` button and a stop button (the latter shuts every session down at
34
- * once, after a confirmation).
32
+ * `×` button shuts the session down on the server. The Terminals
33
+ * section's title toolbar carries a `+` button and a stop button (the
34
+ * latter shuts every session down at once, after a confirmation).
35
+ *
36
+ * The list is one movable section, and other panels' sections can move in.
35
37
  *
36
38
  * The `+` button drops down a menu — built from the launcher's shared
37
39
  * `IAgentRegistry` — listing each available agent (Claude, Codex, …)
@@ -57,9 +59,10 @@ const plugin = {
57
59
  IAgentRegistry,
58
60
  IEditorRegistry,
59
61
  IAgentSessions,
60
- ISettingRegistry
62
+ ISettingRegistry,
63
+ IMovableSectionRegistry
61
64
  ],
62
- activate: (app, tracker, restorer, translator, agentRegistry, editorRegistry, agentSessions, settingRegistry) => {
65
+ activate: (app, tracker, restorer, translator, agentRegistry, editorRegistry, agentSessions, settingRegistry, movableSections) => {
63
66
  const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
64
67
  // Resolve a running agent/editor identifier to an icon: the matching
65
68
  // agent's logo, the matching editor's logo (Neovim/Vim), or the plain
@@ -224,6 +227,14 @@ const plugin = {
224
227
  if (restorer) {
225
228
  restorer.add(panel, panel.id);
226
229
  }
230
+ void app.restored.then(() => {
231
+ panel.expandOwnSection();
232
+ });
233
+ if (movableSections) {
234
+ movableSections.registerSource(PLUGIN_ID, trans.__('Terminals'), panel);
235
+ movableSections.registerTarget(PLUGIN_ID, trans.__('Terminals'), panel);
236
+ panel.announceSections();
237
+ }
227
238
  // The latest-activity line under each agent row is on by default; honour
228
239
  // the `showAgentActivity` setting so it can be turned off. Read on load and
229
240
  // re-applied on every change.
@@ -1,35 +1,39 @@
1
+ import { IMovableSectionDestination, IMovableSectionSource, ISectionEntry } from '@jupyterlab/apputils';
1
2
  import { type TranslationBundle } from '@jupyterlab/translation';
2
- import { LabIcon, ReactWidget } from '@jupyterlab/ui-components';
3
- import * as React from 'react';
3
+ import { LabIcon, SidePanel } from '@jupyterlab/ui-components';
4
+ import { ISignal } from '@lumino/signaling';
5
+ import { AccordionPanel, Widget } from '@lumino/widgets';
4
6
  import { SessionRegistry } from './model';
5
7
  /**
6
- * Left-sidebar panel listing every running terminal session. It is the
7
- * sidebar counterpart of the launcher's terminal tiles: where the
8
- * launcher *starts* sessions, this panel surfaces the ones already
9
- * running so the user can jump back to a backgrounded agent — including
10
- * sessions whose tab has been closed but which are still alive on the
11
- * server. Each row is labelled with the session's title and, for a session
12
- * running a coding agent, a smaller line below it showing the agent's latest
13
- * line of output (from {@link SessionRegistry.activityFor}).
14
- *
15
- * Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
16
- * whenever the registry emits `stateChanged`, so it tracks
17
- * `runningChanged` and the agent-title cache without any local state.
18
- * Click behavior (activate vs. reopen, shutdown, new terminal) is
19
- * delegated to plugin-supplied callbacks so the widget never imports
20
- * `app`.
8
+ * Left-sidebar panel listing every running terminal session, including the
9
+ * sessions whose tab is closed but which are still alive on the server. The
10
+ * list is a single movable "Terminals" section.
21
11
  */
22
- export declare class RunningTerminals extends ReactWidget {
12
+ export declare class RunningTerminals extends SidePanel implements IMovableSectionSource, IMovableSectionDestination {
23
13
  constructor(options: RunningTerminals.IOptions);
14
+ get accordionPanel(): AccordionPanel;
15
+ get sectionAdded(): ISignal<this, ISectionEntry>;
16
+ get sections(): ReadonlyArray<Widget>;
17
+ getSections(): ReadonlyArray<ISectionEntry>;
18
+ removeSectionById(sectionId: string): Widget | null;
19
+ reinsertSection(widget: Widget): void;
20
+ addSection(widget: Widget): void;
21
+ removeSectionWidget(widget: Widget): void;
22
+ /**
23
+ * The layout restorer stores a section as collapsed whenever its panel is off
24
+ * screen at save time, so it can come back collapsed without the user asking.
25
+ */
26
+ expandOwnSection(): void;
27
+ /**
28
+ * The move plugin reconciles persisted moves only against sections announced
29
+ * after `registerSource`, and this panel builds its section in the constructor.
30
+ */
31
+ announceSections(): void;
24
32
  dispose(): void;
25
- protected render(): React.ReactElement;
33
+ private _sectionEntry;
26
34
  private _registry;
27
- private _trans;
28
- private _iconForCommand;
29
- private _onActivate;
30
- private _onShutdown;
31
- private _onShutdownAll;
32
- private _onCreate;
35
+ private _section;
36
+ private _sectionAdded;
33
37
  }
34
38
  export declare namespace RunningTerminals {
35
39
  interface IOptions {
@@ -1,5 +1,6 @@
1
1
  import { nullTranslator } from '@jupyterlab/translation';
2
- import { addIcon, closeIcon, ReactWidget, stopIcon, terminalIcon, UseSignal } from '@jupyterlab/ui-components';
2
+ import { addIcon, closeIcon, PanelWithToolbar, ReactWidget, SidePanel, stopIcon, terminalIcon, ToolbarButton, UseSignal } from '@jupyterlab/ui-components';
3
+ import { Signal } from '@lumino/signaling';
3
4
  import * as React from 'react';
4
5
  /**
5
6
  * Id of the panel widget. Used for layout restoration and as the handle
@@ -7,37 +8,112 @@ import * as React from 'react';
7
8
  */
8
9
  const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
9
10
  /**
10
- * Left-sidebar panel listing every running terminal session. It is the
11
- * sidebar counterpart of the launcher's terminal tiles: where the
12
- * launcher *starts* sessions, this panel surfaces the ones already
13
- * running so the user can jump back to a backgrounded agent — including
14
- * sessions whose tab has been closed but which are still alive on the
15
- * server. Each row is labelled with the session's title and, for a session
16
- * running a coding agent, a smaller line below it showing the agent's latest
17
- * line of output (from {@link SessionRegistry.activityFor}).
18
- *
19
- * Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
20
- * whenever the registry emits `stateChanged`, so it tracks
21
- * `runningChanged` and the agent-title cache without any local state.
22
- * Click behavior (activate vs. reopen, shutdown, new terminal) is
23
- * delegated to plugin-supplied callbacks so the widget never imports
24
- * `app`.
11
+ * Id of the accordion section holding the terminals list. Persisted by the
12
+ * movable-sections plugin, so it must stay stable across releases.
25
13
  */
26
- export class RunningTerminals extends ReactWidget {
14
+ const TERMINALS_SECTION_ID = 'xtralab-terminals-section';
15
+ /**
16
+ * Left-sidebar panel listing every running terminal session, including the
17
+ * sessions whose tab is closed but which are still alive on the server. The
18
+ * list is a single movable "Terminals" section.
19
+ */
20
+ export class RunningTerminals extends SidePanel {
27
21
  constructor(options) {
28
22
  var _a;
29
23
  super();
24
+ this._sectionAdded = new Signal(this);
30
25
  this._registry = options.registry;
31
- this._trans = (_a = options.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab');
32
- this._iconForCommand = options.iconForCommand;
33
- this._onActivate = options.onActivate;
34
- this._onShutdown = options.onShutdown;
35
- this._onShutdownAll = options.onShutdownAll;
36
- this._onCreate = options.onCreate;
26
+ const trans = (_a = options.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab');
37
27
  this.id = RUNNING_TERMINALS_ID;
38
28
  this.title.icon = terminalIcon;
39
- this.title.caption = this._trans.__('Running Terminals');
29
+ this.title.caption = trans.__('Running Terminals');
40
30
  this.addClass('jp-xtralab-Terminals');
31
+ const section = (this._section = new PanelWithToolbar());
32
+ section.id = TERMINALS_SECTION_ID;
33
+ section.title.label = trans.__('Terminals');
34
+ section.addClass('jp-xtralab-Terminals-section');
35
+ const shutdownAll = new ToolbarButton({
36
+ icon: stopIcon,
37
+ onClick: options.onShutdownAll,
38
+ tooltip: trans.__('Shut Down All Terminals'),
39
+ enabled: this._registry.sessionNames().length > 0
40
+ });
41
+ this._registry.stateChanged.connect(() => {
42
+ shutdownAll.enabled = this._registry.sessionNames().length > 0;
43
+ });
44
+ section.toolbar.addItem('shutdown-all', shutdownAll);
45
+ const newTerminal = new ToolbarButton({
46
+ icon: addIcon,
47
+ onClick: () => {
48
+ // Anchor whatever the plugin shows (usually an agent menu) to the
49
+ // button's bottom-left, in the viewport coordinates `Menu.open` wants.
50
+ const rect = newTerminal.node.getBoundingClientRect();
51
+ options.onCreate({ x: rect.left, y: rect.bottom });
52
+ },
53
+ tooltip: trans.__('New Terminal')
54
+ });
55
+ section.toolbar.addItem('new-terminal', newTerminal);
56
+ section.addWidget(new TerminalsListing({
57
+ registry: this._registry,
58
+ trans,
59
+ iconForCommand: options.iconForCommand,
60
+ onActivate: options.onActivate,
61
+ onShutdown: options.onShutdown
62
+ }));
63
+ this.addWidget(section);
64
+ }
65
+ get accordionPanel() {
66
+ return this.content;
67
+ }
68
+ get sectionAdded() {
69
+ return this._sectionAdded;
70
+ }
71
+ get sections() {
72
+ return this.accordionPanel.widgets.filter(w => w !== this._section);
73
+ }
74
+ getSections() {
75
+ const entry = this._sectionEntry();
76
+ return entry ? [entry] : [];
77
+ }
78
+ removeSectionById(sectionId) {
79
+ if (sectionId !== TERMINALS_SECTION_ID ||
80
+ this._section.parent !== this.content) {
81
+ return null;
82
+ }
83
+ this._section.parent = null;
84
+ return this._section;
85
+ }
86
+ reinsertSection(widget) {
87
+ this.addWidget(widget);
88
+ }
89
+ addSection(widget) {
90
+ this.addWidget(widget);
91
+ }
92
+ removeSectionWidget(widget) {
93
+ if (widget.parent === this.content) {
94
+ widget.parent = null;
95
+ }
96
+ }
97
+ /**
98
+ * The layout restorer stores a section as collapsed whenever its panel is off
99
+ * screen at save time, so it can come back collapsed without the user asking.
100
+ */
101
+ expandOwnSection() {
102
+ const accordion = this.accordionPanel;
103
+ const index = Array.from(accordion.widgets).indexOf(this._section);
104
+ if (index >= 0) {
105
+ accordion.expand(index);
106
+ }
107
+ }
108
+ /**
109
+ * The move plugin reconciles persisted moves only against sections announced
110
+ * after `registerSource`, and this panel builds its section in the constructor.
111
+ */
112
+ announceSections() {
113
+ const entry = this._sectionEntry();
114
+ if (entry) {
115
+ this._sectionAdded.emit(entry);
116
+ }
41
117
  }
42
118
  dispose() {
43
119
  if (this.isDisposed) {
@@ -48,54 +124,63 @@ export class RunningTerminals extends ReactWidget {
48
124
  this._registry.dispose();
49
125
  super.dispose();
50
126
  }
127
+ _sectionEntry() {
128
+ const accordion = this.accordionPanel;
129
+ const index = Array.from(accordion.widgets).indexOf(this._section);
130
+ const titleNode = accordion.titles[index];
131
+ if (index < 0 || !titleNode) {
132
+ return null;
133
+ }
134
+ return {
135
+ id: TERMINALS_SECTION_ID,
136
+ titleNode,
137
+ widget: this._section
138
+ };
139
+ }
140
+ }
141
+ /**
142
+ * The body of the Terminals section.
143
+ */
144
+ class TerminalsListing extends ReactWidget {
145
+ constructor(options) {
146
+ super();
147
+ this._options = options;
148
+ this.addClass('jp-xtralab-Terminals-listing');
149
+ }
51
150
  render() {
52
- return (React.createElement(UseSignal, { signal: this._registry.stateChanged }, () => (React.createElement(RunningTerminalsComponent, { registry: this._registry, trans: this._trans, iconForCommand: this._iconForCommand, onActivate: this._onActivate, onShutdown: this._onShutdown, onShutdownAll: this._onShutdownAll, onCreate: this._onCreate }))));
151
+ return (React.createElement(UseSignal, { signal: this._options.registry.stateChanged }, () => (React.createElement(RunningTerminalsComponent, { registry: this._options.registry, trans: this._options.trans, iconForCommand: this._options.iconForCommand, onActivate: this._options.onActivate, onShutdown: this._options.onShutdown }))));
53
152
  }
54
153
  }
55
154
  function RunningTerminalsComponent(props) {
56
- const { registry, trans, iconForCommand, onActivate, onShutdown, onShutdownAll, onCreate } = props;
155
+ const { registry, trans, iconForCommand, onActivate, onShutdown } = props;
57
156
  const names = registry.sessionNames();
58
157
  // The session whose terminal is the current widget in the main area, so its
59
158
  // row can be highlighted. `null` when the current tab is a notebook or any
60
159
  // other non-terminal widget.
61
160
  const currentName = registry.currentSessionName();
62
- return (React.createElement("div", { className: "jp-xtralab-Terminals-body" },
63
- React.createElement("div", { className: "jp-xtralab-Terminals-header" },
64
- React.createElement("h2", { className: "jp-xtralab-Terminals-title" }, trans.__('Terminals')),
65
- React.createElement("div", { className: "jp-xtralab-Terminals-actions" },
66
- React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-action jp-xtralab-Terminals-shutdown-all", onClick: onShutdownAll, disabled: names.length === 0, title: trans.__('Shut Down All Terminals'), "aria-label": trans.__('Shut Down All Terminals') },
67
- React.createElement(stopIcon.react, { tag: "span", verticalAlign: "middle" })),
68
- React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-action jp-xtralab-Terminals-new", onClick: event => {
69
- // Anchor whatever the plugin shows (usually an agent menu) to
70
- // the button's bottom-left, in viewport coordinates — what
71
- // `Menu.open(x, y)` expects.
72
- const rect = event.currentTarget.getBoundingClientRect();
73
- onCreate({ x: rect.left, y: rect.bottom });
74
- }, title: trans.__('New Terminal'), "aria-label": trans.__('New Terminal'), "aria-haspopup": "menu" },
75
- React.createElement(addIcon.react, { tag: "span", verticalAlign: "middle" })))),
76
- names.length === 0 ? (React.createElement("p", { className: "jp-xtralab-Terminals-empty" }, trans.__('No running terminals.'))) : (React.createElement("ul", { className: "jp-xtralab-Terminals-list" }, names.map(name => {
77
- const label = registry.labelFor(name);
78
- const hasWidget = registry.widgetFor(name) !== null;
79
- const tooltip = hasWidget
80
- ? trans.__('Activate %1', label)
81
- : trans.__('Reopen %1', label);
82
- // The running agent's logo (e.g. Claude), or the plain terminal
83
- // icon when nothing recognised is running in the session.
84
- const RowIcon = iconForCommand(registry.agentCommandFor(name)).react;
85
- const isCurrent = name === currentName;
86
- // The latest line of output from the session's agent, shown as a
87
- // smaller line under the title; `null` for rows with nothing to
88
- // surface (no agent running, or no open tab to read a live buffer).
89
- const activity = registry.activityFor(name);
90
- return (React.createElement("li", { key: name, className: isCurrent
91
- ? 'jp-xtralab-Terminals-item jp-mod-current'
92
- : 'jp-xtralab-Terminals-item' },
93
- React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-activate", onClick: () => onActivate(name), title: tooltip, "aria-label": tooltip, "aria-current": isCurrent || undefined },
94
- React.createElement(RowIcon, { tag: "span", className: "jp-xtralab-Terminals-item-icon", verticalAlign: "middle" }),
95
- React.createElement("span", { className: "jp-xtralab-Terminals-item-text" },
96
- React.createElement("span", { className: "jp-xtralab-Terminals-item-label" }, label),
97
- activity ? (React.createElement("span", { className: "jp-xtralab-Terminals-item-detail" }, activity)) : null)),
98
- React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-close", onClick: () => onShutdown(name), title: trans.__('Shut down %1', label), "aria-label": trans.__('Shut down %1', label) },
99
- React.createElement(closeIcon.react, { tag: "span", verticalAlign: "middle" }))));
100
- })))));
161
+ return (React.createElement("div", { className: "jp-xtralab-Terminals-body" }, names.length === 0 ? (React.createElement("p", { className: "jp-xtralab-Terminals-empty" }, trans.__('No running terminals.'))) : (React.createElement("ul", { className: "jp-xtralab-Terminals-list" }, names.map(name => {
162
+ const label = registry.labelFor(name);
163
+ const hasWidget = registry.widgetFor(name) !== null;
164
+ const tooltip = hasWidget
165
+ ? trans.__('Activate %1', label)
166
+ : trans.__('Reopen %1', label);
167
+ // The running agent's logo (e.g. Claude), or the plain terminal
168
+ // icon when nothing recognised is running in the session.
169
+ const RowIcon = iconForCommand(registry.agentCommandFor(name)).react;
170
+ const isCurrent = name === currentName;
171
+ // The latest line of output from the session's agent, shown as a
172
+ // smaller line under the title; `null` for rows with nothing to
173
+ // surface (no agent running, or no open tab to read a live buffer).
174
+ const activity = registry.activityFor(name);
175
+ return (React.createElement("li", { key: name, className: isCurrent
176
+ ? 'jp-xtralab-Terminals-item jp-mod-current'
177
+ : 'jp-xtralab-Terminals-item' },
178
+ React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-activate", onClick: () => onActivate(name), title: tooltip, "aria-label": tooltip, "aria-current": isCurrent || undefined },
179
+ React.createElement(RowIcon, { tag: "span", className: "jp-xtralab-Terminals-item-icon", verticalAlign: "middle" }),
180
+ React.createElement("span", { className: "jp-xtralab-Terminals-item-text" },
181
+ React.createElement("span", { className: "jp-xtralab-Terminals-item-label" }, label),
182
+ activity ? (React.createElement("span", { className: "jp-xtralab-Terminals-item-detail" }, activity)) : null)),
183
+ React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-close", onClick: () => onShutdown(name), title: trans.__('Shut down %1', label), "aria-label": trans.__('Shut down %1', label) },
184
+ React.createElement(closeIcon.react, { tag: "span", verticalAlign: "middle" }))));
185
+ })))));
101
186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtralab",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -8,7 +8,7 @@
8
8
  "properties": {
9
9
  "agents": {
10
10
  "title": "Agents",
11
- "description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, antigravity, copilot, goose, opencode, kiro, mistral-vibe), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Changing a built-in's `command` (for example to a shell alias like `ccm`) keeps the card visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place.",
11
+ "description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, antigravity, copilot, goose, opencode, kiro, mistral-vibe, pi), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Changing a built-in's `command` (for example to a shell alias like `ccm`) keeps the card visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place. Saved entries are merged into the built-in list by id, so this settings view always shows every built-in agent alongside your overrides.",
12
12
  "type": "array",
13
13
  "default": [],
14
14
  "items": {
@@ -69,7 +69,7 @@
69
69
  },
70
70
  "editors": {
71
71
  "title": "Editors",
72
- "description": "Override or extend the terminal editors offered in the launcher's Open section (and recognized in the Terminals panel). Each entry must include an `id`. When the id matches a built-in (nvim, vim), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define additional editors. The launcher shows a single tile — the first editor, by `rank`, whose `command` is on PATH (Neovim before Vim by default). Set `enabled: false` to hide a built-in; disable both to remove the tile entirely. Changing a built-in's `command` (for example to a shell alias) keeps the tile visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place.",
72
+ "description": "Override or extend the terminal editors offered in the launcher's Open section (and recognized in the Terminals panel). Each entry must include an `id`. When the id matches a built-in (nvim, vim), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define additional editors. The launcher shows a single tile — the first editor, by `rank`, whose `command` is on PATH (Neovim before Vim by default). Set `enabled: false` to hide a built-in; disable both to remove the tile entirely. Changing a built-in's `command` (for example to a shell alias) keeps the tile visible even though the alias isn't on PATH; use `requireAvailable: false` only when you want to skip the check without changing the command. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place. Saved entries are merged into the built-in list by id, so this settings view always shows every built-in editor alongside your overrides.",
73
73
  "type": "array",
74
74
  "default": [],
75
75
  "items": {