xtralab 0.9.0 → 0.11.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/README.md +40 -9
- package/lib/commandBar/index.d.ts +14 -0
- package/lib/commandBar/index.js +114 -0
- package/lib/fileBrowser/fileBrowser.js +6 -1
- package/lib/highlight/index.d.ts +15 -0
- package/lib/highlight/index.js +214 -0
- package/lib/index.js +15 -1
- package/lib/launcher/agents.d.ts +7 -0
- package/lib/launcher/agents.js +24 -0
- package/lib/launcher/editorRegistry.js +5 -3
- package/lib/launcher/editors.d.ts +8 -0
- package/lib/launcher/editors.js +17 -0
- package/lib/launcher/index.js +4 -0
- package/lib/launcher/schemaDefaults.d.ts +15 -0
- package/lib/launcher/schemaDefaults.js +44 -0
- package/lib/omnibox/files.d.ts +7 -0
- package/lib/omnibox/files.js +50 -0
- package/lib/omnibox/index.d.ts +14 -0
- package/lib/omnibox/index.js +83 -0
- package/lib/omnibox/model.d.ts +45 -0
- package/lib/omnibox/model.js +142 -0
- package/lib/omnibox/tokens.d.ts +23 -0
- package/lib/omnibox/tokens.js +11 -0
- package/lib/omnibox/widget.d.ts +36 -0
- package/lib/omnibox/widget.js +161 -0
- package/lib/searchReplace/index.d.ts +25 -0
- package/lib/searchReplace/index.js +68 -0
- package/lib/showOutput/index.d.ts +17 -0
- package/lib/showOutput/index.js +154 -0
- package/lib/terminalNotifications/index.d.ts +15 -0
- package/lib/terminalNotifications/index.js +213 -0
- package/lib/terminals/index.d.ts +4 -0
- package/lib/terminals/index.js +65 -16
- package/lib/terminals/model.d.ts +56 -3
- package/lib/terminals/model.js +261 -5
- package/lib/terminals/widget.d.ts +3 -1
- package/lib/terminals/widget.js +11 -3
- package/lib/walkthrough/index.d.ts +14 -0
- package/lib/walkthrough/index.js +162 -0
- package/lib/walkthrough/panel.d.ts +54 -0
- package/lib/walkthrough/panel.js +98 -0
- package/package.json +1 -1
- package/schema/launcher.json +3 -2
- package/schema/terminal-notifications.json +21 -0
- package/schema/terminals.json +15 -0
- package/src/commandBar/index.ts +165 -0
- package/src/fileBrowser/fileBrowser.tsx +8 -1
- package/src/highlight/index.ts +251 -0
- package/src/index.ts +15 -1
- package/src/launcher/agents.ts +25 -0
- package/src/launcher/editorRegistry.ts +8 -4
- package/src/launcher/editors.ts +18 -0
- package/src/launcher/index.ts +4 -0
- package/src/launcher/schemaDefaults.ts +53 -0
- package/src/omnibox/files.ts +68 -0
- package/src/omnibox/index.ts +105 -0
- package/src/omnibox/model.ts +210 -0
- package/src/omnibox/tokens.ts +29 -0
- package/src/omnibox/widget.tsx +322 -0
- package/src/searchReplace/index.ts +86 -0
- package/src/showOutput/index.ts +199 -0
- package/src/terminalNotifications/index.ts +295 -0
- package/src/terminals/index.ts +79 -16
- package/src/terminals/model.ts +304 -3
- package/src/terminals/widget.tsx +21 -4
- package/src/walkthrough/index.ts +202 -0
- package/src/walkthrough/panel.ts +152 -0
- package/style/commandBar.css +104 -0
- package/style/highlight.css +11 -0
- package/style/index.css +5 -0
- package/style/index.js +5 -0
- package/style/omnibox.css +120 -0
- package/style/showOutput.css +11 -0
- package/style/terminals.css +54 -10
- package/style/walkthrough.css +80 -0
package/README.md
CHANGED
|
@@ -34,6 +34,23 @@ from `main` are uploaded as workflow artifacts on the
|
|
|
34
34
|
[CONTRIBUTING.md](./CONTRIBUTING.md) for the desktop app architecture and local
|
|
35
35
|
build instructions.
|
|
36
36
|
|
|
37
|
+
On macOS the build is not notarized yet (it has no Apple Developer ID), so
|
|
38
|
+
Gatekeeper blocks it on first launch, and agent notifications fall back to
|
|
39
|
+
`osascript`, appearing as "Script Editor". In the meantime you can self-sign it
|
|
40
|
+
for free: this lets it launch and gives native notifications instead, branded as
|
|
41
|
+
xtralab and clickable to jump to the terminal that fired them. Create a
|
|
42
|
+
code-signing certificate once in Keychain Access (Certificate Assistant, Create
|
|
43
|
+
a Certificate, with Identity Type "Self Signed Root" and Certificate Type "Code
|
|
44
|
+
Signing") named `xtralab-selfsign`, then sign the app you copied out of the DMG:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
codesign --force --deep --sign "xtralab-selfsign" --timestamp=none "/Applications/xtralab.app"
|
|
48
|
+
xattr -dr com.apple.quarantine "/Applications/xtralab.app"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
macOS prompts once to let `codesign` use the key; choose Always Allow. None of
|
|
52
|
+
this will be needed once the app ships with a Developer ID signature.
|
|
53
|
+
|
|
37
54
|
## What's included
|
|
38
55
|
|
|
39
56
|
xtralab builds on [`ajlab`](https://github.com/jtpio/ajlab), the agent-ready
|
|
@@ -90,13 +107,23 @@ See the [`jupyter-server-mcp` README][mcp] for other MCP clients.
|
|
|
90
107
|
[mcp]: https://github.com/jupyter-ai-contrib/jupyter-server-mcp
|
|
91
108
|
[mcp-spec]: https://modelcontextprotocol.io
|
|
92
109
|
|
|
93
|
-
## Agent
|
|
110
|
+
## Agent skills
|
|
111
|
+
|
|
112
|
+
xtralab ships [Agent Skills][skills] at [`agent-skill/`](./agent-skill) that
|
|
113
|
+
teach any coding agent how to work with the app:
|
|
114
|
+
|
|
115
|
+
- **customize-jupyterlab** turns plain-English requests like "hide the status
|
|
116
|
+
bar", "change the theme", or "add a language server" into the right
|
|
117
|
+
config-file edits.
|
|
118
|
+
- **guided-code-walkthrough** drives the running app over the MCP command
|
|
119
|
+
bridge (see the section above) to open files, jump to and highlight specific
|
|
120
|
+
lines (`xtralab:highlight-lines`), and build a read-only Walkthrough panel of
|
|
121
|
+
prose, snippets, diagrams, and clickable code links beside the editor
|
|
122
|
+
(`xtralab:walkthrough`, no notebook or kernel needed), so the agent can _show_
|
|
123
|
+
you something at your own pace instead of only describing it in chat.
|
|
94
124
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
requests like "hide the status bar", "change the theme", or "add a language
|
|
98
|
-
server". The same SKILL.md file works with Claude Code, Codex CLI, Gemini CLI,
|
|
99
|
-
GitHub Copilot, and other tools that read the Agent Skills format.
|
|
125
|
+
The same SKILL.md files work with Claude Code, Codex CLI, Gemini CLI, GitHub
|
|
126
|
+
Copilot, and other tools that read the Agent Skills format.
|
|
100
127
|
|
|
101
128
|
For Claude Code, this repository doubles as a one-plugin marketplace. Inside a
|
|
102
129
|
Claude Code session:
|
|
@@ -107,12 +134,12 @@ Claude Code session:
|
|
|
107
134
|
```
|
|
108
135
|
|
|
109
136
|
For agents that read from `~/.agents/skills/`, such as Codex CLI or Gemini CLI,
|
|
110
|
-
clone and copy the skill
|
|
137
|
+
clone and copy the skill directories:
|
|
111
138
|
|
|
112
139
|
```bash
|
|
113
140
|
git clone --depth=1 https://github.com/jtpio/xtralab.git /tmp/xtralab
|
|
114
141
|
mkdir -p ~/.agents/skills
|
|
115
|
-
cp -r /tmp/xtralab/agent-skill/skills
|
|
142
|
+
cp -r /tmp/xtralab/agent-skill/skills/* ~/.agents/skills/
|
|
116
143
|
```
|
|
117
144
|
|
|
118
145
|
See [`agent-skill/README.md`](./agent-skill/README.md) for additional install
|
|
@@ -167,7 +194,11 @@ See the [`jupyterlab-lsp` documentation][lsp-config] for the full spec.
|
|
|
167
194
|
## Customizing the launcher
|
|
168
195
|
|
|
169
196
|
Open `Settings → Settings Editor → xtralab launcher` to override, hide, or add
|
|
170
|
-
launcher entries.
|
|
197
|
+
launcher entries. The editor shows xtralab's full built-in list as the default
|
|
198
|
+
for both the agents and editors settings, so you can read every shipped entry
|
|
199
|
+
and copy one into your user preferences to tweak in place. Both lists merge with
|
|
200
|
+
xtralab's defaults by `id`, so an override only needs the `id` plus the fields
|
|
201
|
+
you want to change.
|
|
171
202
|
|
|
172
203
|
### Agents
|
|
173
204
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
/**
|
|
3
|
+
* Add a centered, search-bar-styled command bar to the top area. Clicking it
|
|
4
|
+
* opens the omnibox — a launcher overlay that fuzzy-searches files and
|
|
5
|
+
* commands and routes a typed prompt to an agent (the leading sidebar toggle
|
|
6
|
+
* and the menu bar sit to its left, the trailing sidebar toggle to its right;
|
|
7
|
+
* the bar is centered over the whole panel as an overlay, so it stays put
|
|
8
|
+
* regardless of their widths).
|
|
9
|
+
*
|
|
10
|
+
* The bar is only added when the omnibox is available — the `IOmnibox` token
|
|
11
|
+
* is provided by `xtralab:omnibox` — so it never appears with nothing to open.
|
|
12
|
+
*/
|
|
13
|
+
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
14
|
+
export default plugin;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ILabShell } from '@jupyterlab/application';
|
|
2
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
3
|
+
import { searchIcon } from '@jupyterlab/ui-components';
|
|
4
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
5
|
+
import { Widget } from '@lumino/widgets';
|
|
6
|
+
import { IOmnibox, OMNIBOX_OPEN_COMMAND } from '../omnibox/tokens';
|
|
7
|
+
const PLUGIN_ID = 'xtralab:command-bar';
|
|
8
|
+
/**
|
|
9
|
+
* A search-bar-styled launcher for the `top` area. It holds no text input of
|
|
10
|
+
* its own: clicking the pill (or pressing Enter/Space while it is focused)
|
|
11
|
+
* runs `onActivate`, which opens the omnibox.
|
|
12
|
+
*
|
|
13
|
+
* The widget's root is a transparent, full-height wrapper centered as an
|
|
14
|
+
* overlay over the whole top panel (style/commandBar.css). The wrapper
|
|
15
|
+
* ignores pointer events so only the inner pill is clickable and the empty
|
|
16
|
+
* strips above and below it let clicks fall through to whatever sits beneath
|
|
17
|
+
* the overlay, such as the menu bar.
|
|
18
|
+
*/
|
|
19
|
+
class CommandBar extends Widget {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super({
|
|
22
|
+
node: CommandBar.createNode(options.label, options.caption, options.shortcut)
|
|
23
|
+
});
|
|
24
|
+
this.addClass('jp-xtralab-CommandBar');
|
|
25
|
+
this._onActivate = options.onActivate;
|
|
26
|
+
}
|
|
27
|
+
handleEvent(event) {
|
|
28
|
+
if (event.type === 'click') {
|
|
29
|
+
this._onActivate();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
onAfterAttach() {
|
|
33
|
+
// The pill restores `pointer-events: auto`, so the click bubbles from it
|
|
34
|
+
// up to the root node listened on here; keyboard Enter/Space on the
|
|
35
|
+
// focused button raises the same synthetic click.
|
|
36
|
+
this.node.addEventListener('click', this);
|
|
37
|
+
}
|
|
38
|
+
onBeforeDetach() {
|
|
39
|
+
this.node.removeEventListener('click', this);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
(function (CommandBar) {
|
|
43
|
+
function createNode(label, caption, shortcut) {
|
|
44
|
+
const wrapper = document.createElement('div');
|
|
45
|
+
const button = document.createElement('button');
|
|
46
|
+
button.type = 'button';
|
|
47
|
+
button.className = 'jp-xtralab-CommandBar-button';
|
|
48
|
+
button.title = caption;
|
|
49
|
+
button.setAttribute('aria-label', caption);
|
|
50
|
+
const icon = searchIcon.element({
|
|
51
|
+
tag: 'span',
|
|
52
|
+
className: 'jp-xtralab-CommandBar-icon'
|
|
53
|
+
});
|
|
54
|
+
const text = document.createElement('span');
|
|
55
|
+
text.className = 'jp-xtralab-CommandBar-label';
|
|
56
|
+
text.textContent = label;
|
|
57
|
+
button.appendChild(icon);
|
|
58
|
+
button.appendChild(text);
|
|
59
|
+
if (shortcut) {
|
|
60
|
+
const hint = document.createElement('span');
|
|
61
|
+
hint.className = 'jp-xtralab-CommandBar-shortcut';
|
|
62
|
+
hint.textContent = shortcut;
|
|
63
|
+
// Decorative: the button's aria-label already names the action.
|
|
64
|
+
hint.setAttribute('aria-hidden', 'true');
|
|
65
|
+
button.appendChild(hint);
|
|
66
|
+
}
|
|
67
|
+
wrapper.appendChild(button);
|
|
68
|
+
return wrapper;
|
|
69
|
+
}
|
|
70
|
+
CommandBar.createNode = createNode;
|
|
71
|
+
})(CommandBar || (CommandBar = {}));
|
|
72
|
+
/**
|
|
73
|
+
* Add a centered, search-bar-styled command bar to the top area. Clicking it
|
|
74
|
+
* opens the omnibox — a launcher overlay that fuzzy-searches files and
|
|
75
|
+
* commands and routes a typed prompt to an agent (the leading sidebar toggle
|
|
76
|
+
* and the menu bar sit to its left, the trailing sidebar toggle to its right;
|
|
77
|
+
* the bar is centered over the whole panel as an overlay, so it stays put
|
|
78
|
+
* regardless of their widths).
|
|
79
|
+
*
|
|
80
|
+
* The bar is only added when the omnibox is available — the `IOmnibox` token
|
|
81
|
+
* is provided by `xtralab:omnibox` — so it never appears with nothing to open.
|
|
82
|
+
*/
|
|
83
|
+
const plugin = {
|
|
84
|
+
id: PLUGIN_ID,
|
|
85
|
+
description: 'Add a centered command bar to the top area that opens the omnibox.',
|
|
86
|
+
autoStart: true,
|
|
87
|
+
requires: [ILabShell],
|
|
88
|
+
optional: [IOmnibox, ITranslator],
|
|
89
|
+
activate: (app, labShell, omnibox, translator) => {
|
|
90
|
+
if (!omnibox) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
94
|
+
// Show the omnibox's keyboard shortcut as a hint in the pill, derived from
|
|
95
|
+
// the live binding (registered by xtralab:omnibox, which activates first as
|
|
96
|
+
// the IOmnibox provider) so it stays correct and uses the platform's
|
|
97
|
+
// modifier symbols. Empty when no binding is registered.
|
|
98
|
+
const binding = app.commands.keyBindings.find(keyBinding => keyBinding.command === OMNIBOX_OPEN_COMMAND);
|
|
99
|
+
const shortcut = binding
|
|
100
|
+
? CommandRegistry.formatKeystroke(binding.keys)
|
|
101
|
+
: '';
|
|
102
|
+
const widget = new CommandBar({
|
|
103
|
+
label: trans.__('Search…'),
|
|
104
|
+
caption: trans.__('Search files and commands, or ask an agent'),
|
|
105
|
+
shortcut,
|
|
106
|
+
onActivate: () => {
|
|
107
|
+
omnibox.open();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
widget.id = 'xtralab-command-bar';
|
|
111
|
+
labShell.add(widget, 'top', { rank: 500 });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
export default plugin;
|
|
@@ -64,6 +64,10 @@ const GIT_REPO_PATH = '';
|
|
|
64
64
|
* resolution cost just for one string.
|
|
65
65
|
*/
|
|
66
66
|
const FILE_TREE_TAG = 'file-tree-container';
|
|
67
|
+
const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
|
|
68
|
+
'[data-item-section="spacing-item"] {' +
|
|
69
|
+
'border-left-color: transparent;' +
|
|
70
|
+
'}';
|
|
67
71
|
/**
|
|
68
72
|
* Renders a `@pierre/trees` file tree backed by the Jupyter contents API.
|
|
69
73
|
*
|
|
@@ -80,7 +84,8 @@ export function FileBrowserComponent(props) {
|
|
|
80
84
|
initialExpansion: 'closed',
|
|
81
85
|
search: true,
|
|
82
86
|
icons: FILE_BROWSER_ICONS,
|
|
83
|
-
itemHeight: 24
|
|
87
|
+
itemHeight: 24,
|
|
88
|
+
unsafeCSS: FILE_TREE_UNSAFE_CSS
|
|
84
89
|
});
|
|
85
90
|
React.useEffect(() => {
|
|
86
91
|
const knownDirs = new Map();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
/**
|
|
3
|
+
* Contribute `xtralab:highlight-lines` and `xtralab:clear-highlights`.
|
|
4
|
+
*
|
|
5
|
+
* The built-in command surface can open a file (`docmanager:open`) and move
|
|
6
|
+
* the cursor to a line (`fileeditor:go-to-line`), but it cannot persistently
|
|
7
|
+
* highlight a span of lines — `documentsearch:start` only marks text matches
|
|
8
|
+
* and hijacks the find box. This plugin fills that gap with a CodeMirror line
|
|
9
|
+
* decoration, so an agent can point at "lines 31–43 of src/index.ts" while it
|
|
10
|
+
* narrates a walkthrough. `xtralab:highlight-lines` opens the file when needed,
|
|
11
|
+
* scrolls the range into view, and replaces any previous highlight in that
|
|
12
|
+
* editor; `xtralab:clear-highlights` removes every highlight.
|
|
13
|
+
*/
|
|
14
|
+
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
15
|
+
export default plugin;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
2
|
+
import { CodeMirrorEditor } from '@jupyterlab/codemirror';
|
|
3
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
4
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
5
|
+
import { StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { Decoration, EditorView } from '@codemirror/view';
|
|
7
|
+
const PLUGIN_ID = 'xtralab:highlight';
|
|
8
|
+
/**
|
|
9
|
+
* Highlight a contiguous range of lines, or clear every highlight. These are
|
|
10
|
+
* the only two commands the plugin contributes; both are designed to be driven
|
|
11
|
+
* by a coding agent over the MCP command bridge to walk a user through code.
|
|
12
|
+
*/
|
|
13
|
+
const HIGHLIGHT_LINES_COMMAND = 'xtralab:highlight-lines';
|
|
14
|
+
const CLEAR_HIGHLIGHTS_COMMAND = 'xtralab:clear-highlights';
|
|
15
|
+
/** CodeMirror line-decoration class; styled in style/highlight.css. */
|
|
16
|
+
const HIGHLIGHT_LINE_CLASS = 'jp-xtralab-highlightLine';
|
|
17
|
+
/**
|
|
18
|
+
* A CodeMirror effect carrying the 1-indexed, inclusive line range to
|
|
19
|
+
* highlight, or `null` to clear the editor's highlights.
|
|
20
|
+
*/
|
|
21
|
+
const setHighlight = StateEffect.define();
|
|
22
|
+
/**
|
|
23
|
+
* Holds the highlight decorations for one editor. It is injected lazily (the
|
|
24
|
+
* first time an editor is highlighted) via `StateEffect.appendConfig`, so
|
|
25
|
+
* editors that are never highlighted pay nothing.
|
|
26
|
+
*/
|
|
27
|
+
const highlightField = StateField.define({
|
|
28
|
+
create: () => Decoration.none,
|
|
29
|
+
update(decorations, transaction) {
|
|
30
|
+
// Keep decorations anchored as the user edits around them.
|
|
31
|
+
decorations = decorations.map(transaction.changes);
|
|
32
|
+
for (const effect of transaction.effects) {
|
|
33
|
+
if (!effect.is(setHighlight)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (effect.value === null) {
|
|
37
|
+
decorations = Decoration.none;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const { doc } = transaction.state;
|
|
41
|
+
const lineDecoration = Decoration.line({ class: HIGHLIGHT_LINE_CLASS });
|
|
42
|
+
const start = Math.max(1, Math.min(effect.value.start, doc.lines));
|
|
43
|
+
const end = Math.max(start, Math.min(effect.value.end, doc.lines));
|
|
44
|
+
const ranges = [];
|
|
45
|
+
for (let line = start; line <= end; line++) {
|
|
46
|
+
ranges.push(lineDecoration.range(doc.line(line).from));
|
|
47
|
+
}
|
|
48
|
+
decorations = Decoration.set(ranges);
|
|
49
|
+
}
|
|
50
|
+
return decorations;
|
|
51
|
+
},
|
|
52
|
+
provide: field => EditorView.decorations.from(field)
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Return the CodeMirror view backing a widget's editor, or `null` when the
|
|
56
|
+
* widget is not a CodeMirror-based text editor (a notebook, a terminal, the
|
|
57
|
+
* settings editor, …). Duck-typed on `content.editor` so the plugin does not
|
|
58
|
+
* depend on `@jupyterlab/fileeditor`.
|
|
59
|
+
*/
|
|
60
|
+
function viewForWidget(widget) {
|
|
61
|
+
var _a;
|
|
62
|
+
const editor = (_a = widget === null || widget === void 0 ? void 0 : widget.content) === null || _a === void 0 ? void 0 : _a.editor;
|
|
63
|
+
return editor instanceof CodeMirrorEditor ? editor.editor : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Coerce a JSON command argument to a positive integer line number, falling
|
|
67
|
+
* back to `fallback` when it is missing or not a finite number.
|
|
68
|
+
*/
|
|
69
|
+
function toLine(value, fallback) {
|
|
70
|
+
const n = Math.trunc(Number(value));
|
|
71
|
+
return Number.isFinite(n) && n >= 1 ? n : fallback;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Contribute `xtralab:highlight-lines` and `xtralab:clear-highlights`.
|
|
75
|
+
*
|
|
76
|
+
* The built-in command surface can open a file (`docmanager:open`) and move
|
|
77
|
+
* the cursor to a line (`fileeditor:go-to-line`), but it cannot persistently
|
|
78
|
+
* highlight a span of lines — `documentsearch:start` only marks text matches
|
|
79
|
+
* and hijacks the find box. This plugin fills that gap with a CodeMirror line
|
|
80
|
+
* decoration, so an agent can point at "lines 31–43 of src/index.ts" while it
|
|
81
|
+
* narrates a walkthrough. `xtralab:highlight-lines` opens the file when needed,
|
|
82
|
+
* scrolls the range into view, and replaces any previous highlight in that
|
|
83
|
+
* editor; `xtralab:clear-highlights` removes every highlight.
|
|
84
|
+
*/
|
|
85
|
+
const plugin = {
|
|
86
|
+
id: PLUGIN_ID,
|
|
87
|
+
description: 'Highlight line ranges in text editors to guide walkthroughs.',
|
|
88
|
+
autoStart: true,
|
|
89
|
+
requires: [IDocumentManager],
|
|
90
|
+
optional: [ICommandPalette, ITranslator],
|
|
91
|
+
activate: (app, docManager, palette, translator) => {
|
|
92
|
+
const { commands, shell } = app;
|
|
93
|
+
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
94
|
+
// Editors that currently carry a highlight, so a single clear can reach
|
|
95
|
+
// them all. Disposed editors are dropped on the next clear.
|
|
96
|
+
const highlighted = new Set();
|
|
97
|
+
const ensureField = (view) => {
|
|
98
|
+
if (view.state.field(highlightField, false) === undefined) {
|
|
99
|
+
view.dispatch({ effects: StateEffect.appendConfig.of(highlightField) });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
commands.addCommand(HIGHLIGHT_LINES_COMMAND, {
|
|
103
|
+
label: trans.__('Highlight Lines'),
|
|
104
|
+
caption: trans.__('Highlight a range of lines in a text editor'),
|
|
105
|
+
// Advertise the argument shape so agents listing commands over the MCP
|
|
106
|
+
// bridge can see how to call it, the same way core commands do.
|
|
107
|
+
describedBy: {
|
|
108
|
+
args: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
path: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Path of the file to highlight in, relative to the server root. Opened if needed; defaults to the active editor when omitted.'
|
|
114
|
+
},
|
|
115
|
+
line: {
|
|
116
|
+
type: 'number',
|
|
117
|
+
description: 'First line to highlight (1-indexed). Defaults to 1.'
|
|
118
|
+
},
|
|
119
|
+
endLine: {
|
|
120
|
+
type: 'number',
|
|
121
|
+
description: 'Last line to highlight (1-indexed, inclusive). Defaults to `line` for a single line.'
|
|
122
|
+
},
|
|
123
|
+
reveal: {
|
|
124
|
+
type: 'boolean',
|
|
125
|
+
description: 'Whether to scroll the highlighted range into view. Defaults to true.'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
execute: async (args) => {
|
|
131
|
+
const line = toLine(args['line'], 1);
|
|
132
|
+
const endLine = toLine(args['endLine'], line);
|
|
133
|
+
const path = typeof args['path'] === 'string' ? args['path'] : undefined;
|
|
134
|
+
const reveal = args['reveal'] !== false;
|
|
135
|
+
// With a path, bind strictly to that document: never fall back to the
|
|
136
|
+
// active widget, or an explicit path could silently highlight an
|
|
137
|
+
// unrelated editor that happens to be focused.
|
|
138
|
+
let widget;
|
|
139
|
+
if (path) {
|
|
140
|
+
// Fail fast on a missing path instead of opening a phantom widget
|
|
141
|
+
// behind a blocking "file not found" dialog.
|
|
142
|
+
try {
|
|
143
|
+
await docManager.services.contents.get(path, { content: false });
|
|
144
|
+
}
|
|
145
|
+
catch (_a) {
|
|
146
|
+
throw new Error(`xtralab: no file at "${path}"`);
|
|
147
|
+
}
|
|
148
|
+
const opened = docManager.openOrReveal(path);
|
|
149
|
+
if (!opened) {
|
|
150
|
+
throw new Error(`xtralab: could not open "${path}"`);
|
|
151
|
+
}
|
|
152
|
+
// Wait for the model and the editor view to be ready.
|
|
153
|
+
await opened.context.ready;
|
|
154
|
+
await opened.revealed;
|
|
155
|
+
widget = opened;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
widget = shell.currentWidget;
|
|
159
|
+
}
|
|
160
|
+
const view = viewForWidget(widget);
|
|
161
|
+
if (!view) {
|
|
162
|
+
throw new Error(path
|
|
163
|
+
? `xtralab: "${path}" is not open in a text editor`
|
|
164
|
+
: 'xtralab: the active widget is not a text editor');
|
|
165
|
+
}
|
|
166
|
+
const start = Math.min(line, endLine);
|
|
167
|
+
const end = Math.max(line, endLine);
|
|
168
|
+
ensureField(view);
|
|
169
|
+
const lineCount = view.state.doc.lines;
|
|
170
|
+
const anchor = view.state.doc.line(Math.max(1, Math.min(start, lineCount))).from;
|
|
171
|
+
const effects = [
|
|
172
|
+
setHighlight.of({ start, end })
|
|
173
|
+
];
|
|
174
|
+
if (reveal) {
|
|
175
|
+
effects.push(EditorView.scrollIntoView(anchor, { y: 'center' }));
|
|
176
|
+
}
|
|
177
|
+
view.dispatch({ effects });
|
|
178
|
+
// Forget editors that have since been closed before tracking this one.
|
|
179
|
+
for (const tracked of highlighted) {
|
|
180
|
+
if (!tracked.dom.isConnected) {
|
|
181
|
+
highlighted.delete(tracked);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
highlighted.add(view);
|
|
185
|
+
return start === end
|
|
186
|
+
? trans.__('Highlighted line %1', start)
|
|
187
|
+
: trans.__('Highlighted lines %1-%2', start, end);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
commands.addCommand(CLEAR_HIGHLIGHTS_COMMAND, {
|
|
191
|
+
label: trans.__('Clear Highlights'),
|
|
192
|
+
caption: trans.__('Remove all xtralab line highlights'),
|
|
193
|
+
execute: () => {
|
|
194
|
+
for (const view of highlighted) {
|
|
195
|
+
try {
|
|
196
|
+
if (view.state.field(highlightField, false) !== undefined) {
|
|
197
|
+
view.dispatch({ effects: setHighlight.of(null) });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (_a) {
|
|
201
|
+
// The editor was disposed; nothing to clear.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
highlighted.clear();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
if (palette) {
|
|
208
|
+
const category = trans.__('Other');
|
|
209
|
+
palette.addItem({ command: HIGHLIGHT_LINES_COMMAND, category });
|
|
210
|
+
palette.addItem({ command: CLEAR_HIGHLIGHTS_COMMAND, category });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
export default plugin;
|
package/lib/index.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import agentSessionsPlugin from './agentSessions';
|
|
2
|
+
import commandBarPlugin from './commandBar';
|
|
2
3
|
import fileBrowserPlugin from './fileBrowser';
|
|
3
4
|
import fileTypeIconsPlugin from './fileTypeIcons';
|
|
4
5
|
import editorBreadcrumbsPlugin from './editorBreadcrumbs';
|
|
5
6
|
import editorIndentPlugin from './editorIndent';
|
|
6
7
|
import gitPlugins from './git';
|
|
8
|
+
import highlightPlugin from './highlight';
|
|
7
9
|
import launcherPlugins from './launcher';
|
|
8
10
|
import menusPlugin from './menus';
|
|
11
|
+
import omniboxPlugin from './omnibox';
|
|
12
|
+
import searchReplacePlugin from './searchReplace';
|
|
13
|
+
import showOutputPlugin from './showOutput';
|
|
9
14
|
import sidebarPlugin from './sidebar';
|
|
15
|
+
import terminalNotificationsPlugin from './terminalNotifications';
|
|
10
16
|
import terminalsPlugin from './terminals';
|
|
11
17
|
import topBarPlugin from './topBar';
|
|
18
|
+
import walkthroughPlugin from './walkthrough';
|
|
12
19
|
/**
|
|
13
20
|
* Every plugin contributed by `xtralab`. The entry point of the
|
|
14
21
|
* labextension is an array because the package bundles several independent
|
|
@@ -20,15 +27,22 @@ import topBarPlugin from './topBar';
|
|
|
20
27
|
*/
|
|
21
28
|
const plugins = [
|
|
22
29
|
agentSessionsPlugin,
|
|
30
|
+
commandBarPlugin,
|
|
23
31
|
editorBreadcrumbsPlugin,
|
|
24
32
|
editorIndentPlugin,
|
|
25
33
|
fileBrowserPlugin,
|
|
26
34
|
fileTypeIconsPlugin,
|
|
27
35
|
...gitPlugins,
|
|
36
|
+
highlightPlugin,
|
|
28
37
|
...launcherPlugins,
|
|
29
38
|
menusPlugin,
|
|
39
|
+
omniboxPlugin,
|
|
40
|
+
searchReplacePlugin,
|
|
41
|
+
showOutputPlugin,
|
|
30
42
|
sidebarPlugin,
|
|
43
|
+
terminalNotificationsPlugin,
|
|
31
44
|
terminalsPlugin,
|
|
32
|
-
topBarPlugin
|
|
45
|
+
topBarPlugin,
|
|
46
|
+
walkthroughPlugin
|
|
33
47
|
];
|
|
34
48
|
export default plugins;
|
package/lib/launcher/agents.d.ts
CHANGED
|
@@ -69,6 +69,13 @@ export interface IAgentSettings {
|
|
|
69
69
|
*/
|
|
70
70
|
promptArgs?: string[] | null;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The built-in agents projected into the JSON settings shape
|
|
74
|
+
* ({@link IAgentSettings}), dropping the runtime-only {@link LabIcon}.
|
|
75
|
+
* {@link registerLauncherSchemaDefaults} injects this as the `agents` setting's
|
|
76
|
+
* schema default so the Settings Editor shows the shipped list.
|
|
77
|
+
*/
|
|
78
|
+
export declare function defaultAgentSettings(): IAgentSettings[];
|
|
72
79
|
/**
|
|
73
80
|
* Merge xtralab's defaults with the user's settings. Default entries keep
|
|
74
81
|
* their built-in fields unless explicitly overridden; user-only entries are
|
package/lib/launcher/agents.js
CHANGED
|
@@ -103,6 +103,30 @@ const DEFAULTS = [
|
|
|
103
103
|
promptArgs: []
|
|
104
104
|
}
|
|
105
105
|
];
|
|
106
|
+
/**
|
|
107
|
+
* The built-in agents projected into the JSON settings shape
|
|
108
|
+
* ({@link IAgentSettings}), dropping the runtime-only {@link LabIcon}.
|
|
109
|
+
* {@link registerLauncherSchemaDefaults} injects this as the `agents` setting's
|
|
110
|
+
* schema default so the Settings Editor shows the shipped list.
|
|
111
|
+
*/
|
|
112
|
+
export function defaultAgentSettings() {
|
|
113
|
+
return DEFAULTS.map(agent => {
|
|
114
|
+
const entry = {
|
|
115
|
+
id: agent.id,
|
|
116
|
+
label: agent.label,
|
|
117
|
+
caption: agent.caption,
|
|
118
|
+
command: agent.command,
|
|
119
|
+
rank: agent.rank,
|
|
120
|
+
requireAvailable: agent.requireAvailable
|
|
121
|
+
};
|
|
122
|
+
// Omit `promptArgs` for agents that don't take an inline prompt, so the
|
|
123
|
+
// default doesn't show a prompt form they lack.
|
|
124
|
+
if (agent.promptArgs !== undefined) {
|
|
125
|
+
entry.promptArgs = agent.promptArgs;
|
|
126
|
+
}
|
|
127
|
+
return entry;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
106
130
|
function resolveIcon(id, iconSvg) {
|
|
107
131
|
var _a;
|
|
108
132
|
if (iconSvg) {
|
|
@@ -3,6 +3,7 @@ import { Token } from '@lumino/coreutils';
|
|
|
3
3
|
import { Signal } from '@lumino/signaling';
|
|
4
4
|
import { fetchAvailableCommands } from './availability';
|
|
5
5
|
import { mergeEditors, resolveEditor } from './editors';
|
|
6
|
+
import { LAUNCHER_PLUGIN_ID, registerLauncherSchemaDefaults } from './schemaDefaults';
|
|
6
7
|
/**
|
|
7
8
|
* DI token for {@link IEditorRegistry}. Provided by `xtralab:editor-registry`
|
|
8
9
|
* and consumed — optionally — by `xtralab:launcher` (for its tile) and
|
|
@@ -35,8 +36,6 @@ class EditorRegistry {
|
|
|
35
36
|
this._changed.emit();
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
/** Schema id the `editors` setting lives under (shared with the launcher). */
|
|
39
|
-
const LAUNCHER_SETTINGS_ID = 'xtralab:launcher';
|
|
40
39
|
/**
|
|
41
40
|
* Provides {@link IEditorRegistry}: merges xtralab's built-in editors with the
|
|
42
41
|
* user's `editors` setting, probes the server's `$PATH` to resolve the launcher
|
|
@@ -69,8 +68,11 @@ export const editorRegistryPlugin = {
|
|
|
69
68
|
return Array.isArray(raw) ? raw : [];
|
|
70
69
|
};
|
|
71
70
|
if (settingRegistry) {
|
|
71
|
+
// Must precede the first `load`: the schema defers loading until a
|
|
72
|
+
// transform is registered.
|
|
73
|
+
registerLauncherSchemaDefaults(settingRegistry);
|
|
72
74
|
try {
|
|
73
|
-
const settings = await settingRegistry.load(
|
|
75
|
+
const settings = await settingRegistry.load(LAUNCHER_PLUGIN_ID);
|
|
74
76
|
await apply(readOverrides(settings));
|
|
75
77
|
settings.changed.connect(async () => {
|
|
76
78
|
try {
|
|
@@ -56,6 +56,14 @@ export interface IEditorSettings {
|
|
|
56
56
|
/** See {@link IEditor.requireAvailable}. */
|
|
57
57
|
requireAvailable?: boolean;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The built-in editors projected into the JSON settings shape
|
|
61
|
+
* ({@link IEditorSettings}), dropping the runtime-only {@link LabIcon}. Mirrors
|
|
62
|
+
* {@link defaultAgentSettings}: {@link registerLauncherSchemaDefaults} injects
|
|
63
|
+
* this as the `editors` setting's schema default so the Settings Editor shows
|
|
64
|
+
* the shipped list.
|
|
65
|
+
*/
|
|
66
|
+
export declare function defaultEditorSettings(): IEditorSettings[];
|
|
59
67
|
/**
|
|
60
68
|
* Merge xtralab's built-in editors with the user's `editors` settings. Built-in
|
|
61
69
|
* entries keep their fields unless explicitly overridden; user-only entries are
|
package/lib/launcher/editors.js
CHANGED
|
@@ -26,6 +26,23 @@ const DEFAULTS = [
|
|
|
26
26
|
requireAvailable: true
|
|
27
27
|
}
|
|
28
28
|
];
|
|
29
|
+
/**
|
|
30
|
+
* The built-in editors projected into the JSON settings shape
|
|
31
|
+
* ({@link IEditorSettings}), dropping the runtime-only {@link LabIcon}. Mirrors
|
|
32
|
+
* {@link defaultAgentSettings}: {@link registerLauncherSchemaDefaults} injects
|
|
33
|
+
* this as the `editors` setting's schema default so the Settings Editor shows
|
|
34
|
+
* the shipped list.
|
|
35
|
+
*/
|
|
36
|
+
export function defaultEditorSettings() {
|
|
37
|
+
return DEFAULTS.map(editor => ({
|
|
38
|
+
id: editor.id,
|
|
39
|
+
label: editor.label,
|
|
40
|
+
caption: editor.caption,
|
|
41
|
+
command: editor.command,
|
|
42
|
+
rank: editor.rank,
|
|
43
|
+
requireAvailable: editor.requireAvailable
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
29
46
|
function resolveEditorIcon(id, iconSvg) {
|
|
30
47
|
var _a;
|
|
31
48
|
if (iconSvg) {
|
package/lib/launcher/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { CREATE_LAUNCHER_COMMAND, registerAgentCommands } from './commands';
|
|
|
11
11
|
import { LauncherDashboard } from './dashboard';
|
|
12
12
|
import { editorRegistryPlugin, IEditorRegistry } from './editorRegistry';
|
|
13
13
|
import { AgentRegistry } from './registry';
|
|
14
|
+
import { registerLauncherSchemaDefaults } from './schemaDefaults';
|
|
14
15
|
import { IAgentRegistry } from './tokens';
|
|
15
16
|
const PLUGIN_ID = 'xtralab:launcher';
|
|
16
17
|
/**
|
|
@@ -77,6 +78,9 @@ const plugin = {
|
|
|
77
78
|
return Array.isArray(raw) ? raw : [];
|
|
78
79
|
};
|
|
79
80
|
if (settingRegistry) {
|
|
81
|
+
// Must precede the first `load`: the schema defers loading until a
|
|
82
|
+
// transform is registered.
|
|
83
|
+
registerLauncherSchemaDefaults(settingRegistry);
|
|
80
84
|
try {
|
|
81
85
|
const settings = await settingRegistry.load(PLUGIN_ID);
|
|
82
86
|
await applyAgents(readOverrides(settings));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
2
|
+
/** Schema id the launcher's `agents`/`editors` settings live under. */
|
|
3
|
+
export declare const LAUNCHER_PLUGIN_ID = "xtralab:launcher";
|
|
4
|
+
/**
|
|
5
|
+
* Register a `fetch` transform that injects xtralab's built-in agent and
|
|
6
|
+
* editor lists as the schema defaults for the `xtralab:launcher` settings, so
|
|
7
|
+
* the Settings Editor shows the shipped list.
|
|
8
|
+
*
|
|
9
|
+
* The schema declares `jupyter.lab.transform: true`, so the registry defers
|
|
10
|
+
* loading the plugin until a transform is registered: callers must invoke this
|
|
11
|
+
* before their first `settingRegistry.load(LAUNCHER_PLUGIN_ID)`. Idempotent, so
|
|
12
|
+
* the two plugins that load these settings can both call it regardless of
|
|
13
|
+
* activation order.
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerLauncherSchemaDefaults(settingRegistry: ISettingRegistry): void;
|