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
@@ -1,23 +1,28 @@
1
1
  import * as React from 'react';
2
- import { ReactWidget, Toolbar } from '@jupyterlab/ui-components';
2
+ import { AccordionToolbar, PanelWithToolbar, ReactWidget, Toolbar } from '@jupyterlab/ui-components';
3
3
  import { Signal } from '@lumino/signaling';
4
- import { PanelLayout, Widget } from '@lumino/widgets';
4
+ import { AccordionPanel, PanelLayout, Widget } from '@lumino/widgets';
5
5
  import { FileBrowserComponent } from './fileBrowser';
6
6
  import { xtralabFileBrowserIcon } from './icons';
7
7
  export const FILE_BROWSER_ID = 'xtralab:file-browser';
8
8
  /**
9
- * The CSS class added to the xtralab file browser widget. The selectors that
10
- * bind application context menu items hang off this class, so it must remain
11
- * specific enough not to clash with other browsers (such as the default
12
- * `jp-FileBrowser`).
9
+ * Persisted by the movable-sections plugin, so it must stay stable.
10
+ */
11
+ const TREE_SECTION_ID = 'xtralab-file-tree-section';
12
+ /**
13
+ * Context menu selectors hang off this class, so it sits on the section and
14
+ * keeps matching once the tree moves to another panel.
13
15
  */
14
16
  const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser';
17
+ const PANEL_CSS_CLASS = 'jp-xtralab-FileBrowserPanel';
15
18
  const TOOLBAR_CSS_CLASS = 'jp-xtralab-FileBrowser-toolbar';
16
19
  const CONTENT_CSS_CLASS = 'jp-xtralab-FileBrowser-content';
20
+ const ACCORDION_CSS_CLASS = 'jp-xtralab-FileBrowser-accordion';
21
+ const MOVABLE_SECTION_CLASS = 'jp-movable-section';
17
22
  /**
18
- * Lumino widget that hosts the React-based `@pierre/trees` file browser.
19
- * The widget is laid out with a JupyterLab `Toolbar` on top and the tree
20
- * filling the remaining space.
23
+ * Lumino widget hosting the React-based `@pierre/trees` file browser. The tree
24
+ * and its toolbar form one movable "Files" section, and sections from other
25
+ * panels can be moved in below it.
21
26
  */
22
27
  export class XtralabFileBrowser extends Widget {
23
28
  constructor(options) {
@@ -31,26 +36,37 @@ export class XtralabFileBrowser extends Widget {
31
36
  this._collapseAllRequested = new Signal(this);
32
37
  this._fileFilterVisible = false;
33
38
  this._fileFilterVisibleChanged = new Signal(this);
34
- const layout = (this.layout = new PanelLayout());
39
+ this._accordion = null;
40
+ this._contentChanged = new Signal(this);
41
+ this._sectionAdded = new Signal(this);
42
+ const layout = (this._panelLayout = this.layout = new PanelLayout());
35
43
  this._contentsManager = options.contentsManager;
36
44
  this._docManager = options.docManager;
37
45
  this._onOpenFile = options.onOpenFile;
46
+ this._translator = options.translator;
38
47
  this.id = FILE_BROWSER_ID;
39
48
  this.title.icon = xtralabFileBrowserIcon;
40
49
  this.title.caption = 'xtralab File Browser';
41
- this.addClass(FILE_BROWSER_CSS_CLASS);
50
+ this.addClass(PANEL_CSS_CLASS);
42
51
  this._toolbar = new Toolbar();
43
52
  this._toolbar.addClass(TOOLBAR_CSS_CLASS);
44
53
  this._toolbar.node.setAttribute('aria-label', 'file browser');
45
- layout.addWidget(this._toolbar);
46
54
  this._content = new XtralabFileTreeContent({
47
55
  contentsManager: this._contentsManager,
48
56
  docManager: this._docManager,
49
57
  onOpenFile: this._onOpenFile,
58
+ translator: this._translator,
50
59
  browser: this
51
60
  });
52
61
  this._content.addClass(CONTENT_CSS_CLASS);
53
- layout.addWidget(this._content);
62
+ this._section = new PanelWithToolbar({ toolbar: this._toolbar });
63
+ this._section.id = TREE_SECTION_ID;
64
+ // Only an accordion host draws this; a plain `PanelLayout` shows no title.
65
+ this._section.title.label = 'Files';
66
+ this._section.addClass(FILE_BROWSER_CSS_CLASS);
67
+ this._section.addWidget(this._toolbar);
68
+ this._section.addWidget(this._content);
69
+ layout.addWidget(this._section);
54
70
  }
55
71
  get contentsManager() {
56
72
  return this._contentsManager;
@@ -121,6 +137,129 @@ export class XtralabFileBrowser extends Widget {
121
137
  toggleFileFilter() {
122
138
  this.setFileFilterVisible(!this._fileFilterVisible);
123
139
  }
140
+ get sectionNode() {
141
+ return this._section.node;
142
+ }
143
+ get isEmpty() {
144
+ return this._panelLayout.widgets.length === 0;
145
+ }
146
+ get contentChanged() {
147
+ return this._contentChanged;
148
+ }
149
+ get sectionAdded() {
150
+ return this._sectionAdded;
151
+ }
152
+ get accordionPanel() {
153
+ return this._accordion;
154
+ }
155
+ get sections() {
156
+ if (!this._accordion) {
157
+ return [];
158
+ }
159
+ return this._accordion.widgets.filter(w => w !== this._section);
160
+ }
161
+ /**
162
+ * The toolbar is the section's title node: it is the tree's header row at
163
+ * home, and an accordion host renders it inside the section title.
164
+ */
165
+ getSections() {
166
+ return this._isSectionHome()
167
+ ? [
168
+ {
169
+ id: TREE_SECTION_ID,
170
+ titleNode: this._toolbar.node,
171
+ widget: this._section
172
+ }
173
+ ]
174
+ : [];
175
+ }
176
+ removeSectionById(sectionId) {
177
+ if (sectionId !== TREE_SECTION_ID || !this._isSectionHome()) {
178
+ return null;
179
+ }
180
+ this._section.parent = null;
181
+ if (this._accordion && this._accordion.widgets.length === 0) {
182
+ this._accordion.dispose();
183
+ this._accordion = null;
184
+ }
185
+ // Away from home the toolbar must not offer "Move to …" as well;
186
+ // `getSections` re-marks it on the way back.
187
+ this._toolbar.node.classList.remove(MOVABLE_SECTION_CLASS);
188
+ this._contentChanged.emit();
189
+ return this._section;
190
+ }
191
+ reinsertSection(widget) {
192
+ if (this._accordion) {
193
+ this._accordion.insertWidget(0, widget);
194
+ }
195
+ else {
196
+ this._panelLayout.addWidget(widget);
197
+ this._restoreToolbar();
198
+ }
199
+ this._contentChanged.emit();
200
+ }
201
+ /**
202
+ * The move plugin reconciles persisted moves only against sections announced
203
+ * after `registerSource`, and this panel builds its section in the constructor.
204
+ */
205
+ announceSections() {
206
+ for (const entry of this.getSections()) {
207
+ this._sectionAdded.emit(entry);
208
+ }
209
+ }
210
+ /**
211
+ * The first hosted section moves the tree into an accordion.
212
+ */
213
+ addSection(widget) {
214
+ if (!this._accordion) {
215
+ const accordion = new AccordionPanel({
216
+ layout: AccordionToolbar.createLayout({})
217
+ });
218
+ accordion.addClass(ACCORDION_CSS_CLASS);
219
+ if (this._section.parent === this) {
220
+ accordion.addWidget(this._section);
221
+ }
222
+ accordion.addWidget(widget);
223
+ this._accordion = accordion;
224
+ this._panelLayout.addWidget(accordion);
225
+ }
226
+ else {
227
+ this._accordion.addWidget(widget);
228
+ }
229
+ this._contentChanged.emit();
230
+ }
231
+ removeSectionWidget(widget) {
232
+ if (!this._accordion || widget.parent !== this._accordion) {
233
+ return;
234
+ }
235
+ widget.parent = null;
236
+ const remaining = this._accordion.widgets;
237
+ const treeOnly = remaining.length === 1 && remaining[0] === this._section;
238
+ if (treeOnly || remaining.length === 0) {
239
+ if (treeOnly) {
240
+ if (this._section.isHidden) {
241
+ this._section.show();
242
+ }
243
+ this._panelLayout.addWidget(this._section);
244
+ this._restoreToolbar();
245
+ }
246
+ this._accordion.dispose();
247
+ this._accordion = null;
248
+ }
249
+ this._contentChanged.emit();
250
+ }
251
+ _isSectionHome() {
252
+ const parent = this._section.parent;
253
+ return parent === this || (!!this._accordion && parent === this._accordion);
254
+ }
255
+ /**
256
+ * An accordion host moves the toolbar node into the section title and leaves
257
+ * it orphaned on detach; re-parenting makes the layout attach it again.
258
+ */
259
+ _restoreToolbar() {
260
+ this._toolbar.parent = null;
261
+ this._section.insertWidget(0, this._toolbar);
262
+ }
124
263
  }
125
264
  /**
126
265
  * The React-rendered region of the file browser. Lives inside the host
@@ -132,6 +271,6 @@ class XtralabFileTreeContent extends ReactWidget {
132
271
  this._options = options;
133
272
  }
134
273
  render() {
135
- return (React.createElement(FileBrowserComponent, { contentsManager: this._options.contentsManager, docManager: this._options.docManager, onOpenFile: this._options.onOpenFile, widget: this._options.browser }));
274
+ return (React.createElement(FileBrowserComponent, { contentsManager: this._options.contentsManager, docManager: this._options.docManager, onOpenFile: this._options.onOpenFile, translator: this._options.translator, widget: this._options.browser }));
136
275
  }
137
276
  }
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@ import aboutPlugin from './about';
2
2
  import agentSessionsPlugin from './agentSessions';
3
3
  import askAgentPlugin from './askAgent';
4
4
  import commandBarPlugin from './commandBar';
5
+ import customPanelPlugin from './customPanel';
5
6
  import fileBrowserPlugin from './fileBrowser';
6
7
  import fileTypeIconsPlugin from './fileTypeIcons';
7
8
  import editorBreadcrumbsPlugin from './editorBreadcrumbs';
@@ -33,6 +34,7 @@ const plugins = [
33
34
  agentSessionsPlugin,
34
35
  askAgentPlugin,
35
36
  commandBarPlugin,
37
+ customPanelPlugin,
36
38
  editorBreadcrumbsPlugin,
37
39
  editorIndentPlugin,
38
40
  fileBrowserPlugin,
@@ -23,7 +23,7 @@ export interface IAgent {
23
23
  * the user types one into the launcher's prompt box. The semantics:
24
24
  *
25
25
  * - `[]` → prompt is appended as a positional argument:
26
- * `<command> 'PROMPT'`. (Used by claude, codex, vibe.)
26
+ * `<command> 'PROMPT'`. (Used by claude, codex, vibe, pi.)
27
27
  * - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
28
28
  * `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
29
29
  * - `undefined` → the agent does not accept an initial prompt. The
@@ -7,8 +7,9 @@ import { BUILTIN_AGENT_ICONS } from './icons';
7
7
  * icons come from), but use the bare CLI names a user would type in a
8
8
  * terminal rather than the ACP wrapper binaries that project spawns.
9
9
  * Antigravity (Google's agent-first IDE/CLI, `agy`) is the Google agent.
10
- * Anything not on the user's `$PATH` is filtered out at activation time, so
11
- * the wider list is harmless.
10
+ * Pi (https://pi.dev, `earendil-works/pi`) is a minimal extensible coding
11
+ * agent; its mark comes straight from its site. Anything not on the user's
12
+ * `$PATH` is filtered out at activation time, so the wider list is harmless.
12
13
  */
13
14
  const DEFAULTS = [
14
15
  {
@@ -100,6 +101,18 @@ const DEFAULTS = [
100
101
  rank: 7,
101
102
  requireAvailable: true,
102
103
  promptArgs: []
104
+ },
105
+ {
106
+ id: 'pi',
107
+ label: 'Pi',
108
+ caption: 'Start Pi in a new terminal.',
109
+ command: 'pi',
110
+ icon: BUILTIN_AGENT_ICONS.pi,
111
+ rank: 8,
112
+ requireAvailable: true,
113
+ // `pi [messages...]` starts the interactive TUI and sends the messages
114
+ // as the first user prompt, so the positional form applies.
115
+ promptArgs: []
103
116
  }
104
117
  ];
105
118
  /**
@@ -5,11 +5,14 @@ import { LabIcon } from '@jupyterlab/ui-components';
5
5
  * (`jupyter_ai_acp_client/static/*.svg`, BSD-3-Clause); Antigravity post-dates
6
6
  * that set, so its mark is the official glyph from `@lobehub/icons-static-svg`
7
7
  * (MIT) — we ship the brand-colored `antigravity-color` variant so Antigravity
8
- * reads as a multi-color Google mark. Small adjustments are applied
8
+ * reads as a multi-color Google mark. Pi's mark is the official `logo-auto.svg`
9
+ * from https://pi.dev (MIT, `earendil-works/pi`). Small adjustments are applied
9
10
  * to play nicely with JupyterLab themes:
10
- * - Monochrome marks (Codex, Copilot, Goose) use `currentColor` so they
11
+ * - Monochrome marks (Codex, Copilot, Goose, Pi) use `currentColor` so they
11
12
  * inherit the surrounding launcher card's text color rather than
12
- * hard-coding black, which disappears on the dark theme.
13
+ * hard-coding black, which disappears on the dark theme. For Pi this
14
+ * replaces the upstream `prefers-color-scheme` style block, which follows
15
+ * the OS scheme rather than the active JupyterLab theme.
13
16
  * - The Goose mark upstream is rendered on a white rounded rect; we drop
14
17
  * the rect so the silhouette can sit on the launcher card directly.
15
18
  * - SVG ids that would collide if two copies of the same artwork ended up
@@ -169,6 +172,13 @@ const opencodeIcon = new LabIcon({
169
172
  <path fill-rule="evenodd" clip-rule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" fill="white"/>
170
173
  </svg>`
171
174
  });
175
+ const piIcon = new LabIcon({
176
+ name: 'xtralab:agent-pi',
177
+ svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" fill="currentColor">
178
+ <path fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/>
179
+ <path d="M517.36 400H634.72V634.72H517.36Z"/>
180
+ </svg>`
181
+ });
172
182
  const neovimIcon = new LabIcon({
173
183
  name: 'xtralab:editor-neovim',
174
184
  svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#57A143">
@@ -195,7 +205,8 @@ export const BUILTIN_AGENT_ICONS = {
195
205
  goose: gooseIcon,
196
206
  kiro: kiroIcon,
197
207
  'mistral-vibe': mistralVibeIcon,
198
- opencode: opencodeIcon
208
+ opencode: opencodeIcon,
209
+ pi: piIcon
199
210
  };
200
211
  /**
201
212
  * Built-in icons keyed by the editor's id, used to resolve the icon for a
@@ -4,14 +4,7 @@ import type { ISettingRegistry } from '@jupyterlab/settingregistry';
4
4
  */
5
5
  export declare const LAUNCHER_PLUGIN_ID = "xtralab:launcher";
6
6
  /**
7
- * Register a `fetch` transform that injects xtralab's built-in agent and
8
- * editor lists as the schema defaults for the `xtralab:launcher` settings, so
9
- * the Settings Editor shows the shipped list.
10
- *
11
- * The schema declares `jupyter.lab.transform: true`, so the registry defers
12
- * loading the plugin until a transform is registered: callers must invoke this
13
- * before their first `settingRegistry.load(LAUNCHER_PLUGIN_ID)`. Idempotent, so
14
- * the two plugins that load these settings can both call it regardless of
15
- * activation order.
7
+ * Must run before the first `load(LAUNCHER_PLUGIN_ID)` (the schema sets
8
+ * `jupyter.lab.transform: true`); idempotent across callers.
16
9
  */
17
10
  export declare function registerLauncherSchemaDefaults(settingRegistry: ISettingRegistry): void;
@@ -5,20 +5,38 @@ import { defaultEditorSettings } from './editors';
5
5
  */
6
6
  export const LAUNCHER_PLUGIN_ID = 'xtralab:launcher';
7
7
  /**
8
- * Setting registries that already have the transform, so both callers skip
9
- * re-registering (the registry rejects a duplicate transformer).
8
+ * Registries that already have the transform (duplicates are rejected).
10
9
  */
11
10
  const registered = new WeakSet();
12
11
  /**
13
- * Register a `fetch` transform that injects xtralab's built-in agent and
14
- * editor lists as the schema defaults for the `xtralab:launcher` settings, so
15
- * the Settings Editor shows the shipped list.
16
- *
17
- * The schema declares `jupyter.lab.transform: true`, so the registry defers
18
- * loading the plugin until a transform is registered: callers must invoke this
19
- * before their first `settingRegistry.load(LAUNCHER_PLUGIN_ID)`. Idempotent, so
20
- * the two plugins that load these settings can both call it regardless of
21
- * activation order.
12
+ * Merge sparse user entries over the built-in defaults, keyed by `id`.
13
+ */
14
+ function mergeSettingsById(defaults, user) {
15
+ const entries = Array.isArray(user) ? user : [];
16
+ const overrideById = new Map(entries.map(entry => [entry.id, entry]));
17
+ const merged = defaults.map(base => {
18
+ const override = overrideById.get(base.id);
19
+ if (!override) {
20
+ return base;
21
+ }
22
+ overrideById.delete(base.id);
23
+ return { ...base, ...override };
24
+ });
25
+ merged.push(...overrideById.values());
26
+ // Item schema defaults are applied before this transform runs; without the
27
+ // refill, sparse entries render with unchecked boxes.
28
+ return merged.map(entry => {
29
+ var _a, _b;
30
+ return ({
31
+ ...entry,
32
+ enabled: (_a = entry.enabled) !== null && _a !== void 0 ? _a : true,
33
+ requireAvailable: (_b = entry.requireAvailable) !== null && _b !== void 0 ? _b : true
34
+ });
35
+ });
36
+ }
37
+ /**
38
+ * Must run before the first `load(LAUNCHER_PLUGIN_ID)` (the schema sets
39
+ * `jupyter.lab.transform: true`); idempotent across callers.
22
40
  */
23
41
  export function registerLauncherSchemaDefaults(settingRegistry) {
24
42
  if (registered.has(settingRegistry)) {
@@ -26,11 +44,17 @@ export function registerLauncherSchemaDefaults(settingRegistry) {
26
44
  }
27
45
  registered.add(settingRegistry);
28
46
  settingRegistry.transform(LAUNCHER_PLUGIN_ID, {
47
+ compose: plugin => {
48
+ const { user } = plugin.data;
49
+ const composite = { ...plugin.data.composite };
50
+ composite.agents = mergeSettingsById(defaultAgentSettings(), user.agents);
51
+ composite.editors = mergeSettingsById(defaultEditorSettings(), user.editors);
52
+ plugin.data = { composite, user };
53
+ return plugin;
54
+ },
29
55
  fetch: plugin => {
30
56
  const properties = plugin.schema.properties;
31
57
  if (properties) {
32
- // The settings shapes are plain JSON by construction, but their named
33
- // interfaces lack an index signature, so cast at this TS→JSON boundary.
34
58
  if (properties.agents) {
35
59
  properties.agents.default =
36
60
  defaultAgentSettings();
@@ -1,18 +1,12 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
  /**
3
- * Lets the user toggle individual left-sidebar tabs from
4
- * View > Appearance > Left Sidebar. Removing a widget from a `LabShell`
5
- * side area is done by setting `widget.parent = null`: the StackedPanel
6
- * emits `widgetRemoved`, which the SideBarHandler responds to by removing
7
- * the matching tab. The widget instance is preserved, so we can re-add it
8
- * later via `labShell.add` with its original rank.
9
- *
10
- * The menu placement is contributed declaratively in
11
- * `schema/sidebar.json`, so this plugin only registers commands and
12
- * applies state — it does not touch `IMainMenu` directly.
13
- *
14
- * The preference is persisted to `xtralab:sidebar` settings so the choice
15
- * survives reloads.
3
+ * Toggles individual sidebar tabs from View > Appearance > Sidebars,
4
+ * whichever side a tab currently lives on. Hiding detaches the widget
5
+ * (`widget.parent = null`) while keeping the instance for a later
6
+ * re-add; the preference persists in this plugin's settings. Which side
7
+ * a hidden tab came from is only tracked in-memory, so after a reload a
8
+ * re-shown tab lands on the left. The menu placement is contributed
9
+ * declaratively in `schema/sidebar.json`.
16
10
  */
17
11
  declare const plugin: JupyterFrontEndPlugin<void>;
18
12
  export default plugin;
@@ -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, …)