xtralab 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/customPanel/index.d.ts +7 -0
- package/lib/customPanel/index.js +74 -0
- package/lib/fileBrowser/commands.js +13 -1
- package/lib/fileBrowser/contents.d.ts +11 -0
- package/lib/fileBrowser/contents.js +28 -0
- package/lib/fileBrowser/dragAndDrop.d.ts +66 -0
- package/lib/fileBrowser/dragAndDrop.js +115 -0
- package/lib/fileBrowser/fileBrowser.d.ts +2 -0
- package/lib/fileBrowser/fileBrowser.js +151 -197
- package/lib/fileBrowser/index.js +39 -6
- package/lib/fileBrowser/widget.d.ts +44 -5
- package/lib/fileBrowser/widget.js +153 -14
- package/lib/index.js +2 -0
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +15 -2
- package/lib/launcher/icons.js +15 -4
- package/lib/launcher/schemaDefaults.d.ts +2 -9
- package/lib/launcher/schemaDefaults.js +37 -13
- package/lib/sidebar/index.d.ts +7 -13
- package/lib/sidebar/index.js +80 -46
- package/lib/terminals/index.d.ts +5 -3
- package/lib/terminals/index.js +17 -6
- package/lib/terminals/widget.d.ts +29 -25
- package/lib/terminals/widget.js +150 -65
- package/package.json +1 -1
- package/schema/launcher.json +2 -2
- package/schema/sidebar.json +47 -7
- package/src/customPanel/index.ts +104 -0
- package/src/fileBrowser/commands.ts +14 -1
- package/src/fileBrowser/contents.ts +30 -0
- package/src/fileBrowser/dragAndDrop.ts +178 -0
- package/src/fileBrowser/fileBrowser.tsx +183 -225
- package/src/fileBrowser/index.ts +41 -6
- package/src/fileBrowser/widget.tsx +195 -14
- package/src/index.ts +2 -0
- package/src/launcher/agents.ts +16 -3
- package/src/launcher/icons.ts +16 -4
- package/src/launcher/schemaDefaults.ts +43 -13
- package/src/sidebar/index.ts +95 -52
- package/src/terminals/index.ts +24 -6
- package/src/terminals/widget.tsx +186 -87
- package/style/base.css +22 -34
- package/style/customPanel.css +9 -0
- package/style/index.js +1 -0
- package/style/terminals.css +19 -77
|
@@ -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
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
package/lib/launcher/agents.d.ts
CHANGED
|
@@ -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
|
package/lib/launcher/agents.js
CHANGED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
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
|
/**
|
package/lib/launcher/icons.js
CHANGED
|
@@ -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.
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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();
|
package/lib/sidebar/index.d.ts
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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;
|
package/lib/sidebar/index.js
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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
|
-
//
|
|
46
|
-
//
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 =
|
|
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
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
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 && !
|
|
88
|
-
labShell.add(widget, 'left', {
|
|
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 &&
|
|
91
|
-
|
|
92
|
-
//
|
|
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`
|
|
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
|
|
121
|
-
//
|
|
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
|
|
144
|
-
// recoverable
|
|
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
|
}
|
package/lib/terminals/index.d.ts
CHANGED
|
@@ -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
|
|
21
|
-
* `+` button and a stop button (the
|
|
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, …)
|