xtralab 0.10.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.
Files changed (71) hide show
  1. package/README.md +23 -9
  2. package/lib/commandBar/index.d.ts +14 -0
  3. package/lib/commandBar/index.js +114 -0
  4. package/lib/fileBrowser/fileBrowser.js +6 -1
  5. package/lib/highlight/index.d.ts +15 -0
  6. package/lib/highlight/index.js +214 -0
  7. package/lib/index.js +13 -1
  8. package/lib/launcher/agents.d.ts +7 -0
  9. package/lib/launcher/agents.js +24 -0
  10. package/lib/launcher/editorRegistry.js +5 -3
  11. package/lib/launcher/editors.d.ts +8 -0
  12. package/lib/launcher/editors.js +17 -0
  13. package/lib/launcher/index.js +4 -0
  14. package/lib/launcher/schemaDefaults.d.ts +15 -0
  15. package/lib/launcher/schemaDefaults.js +44 -0
  16. package/lib/omnibox/files.d.ts +7 -0
  17. package/lib/omnibox/files.js +50 -0
  18. package/lib/omnibox/index.d.ts +14 -0
  19. package/lib/omnibox/index.js +83 -0
  20. package/lib/omnibox/model.d.ts +45 -0
  21. package/lib/omnibox/model.js +142 -0
  22. package/lib/omnibox/tokens.d.ts +23 -0
  23. package/lib/omnibox/tokens.js +11 -0
  24. package/lib/omnibox/widget.d.ts +36 -0
  25. package/lib/omnibox/widget.js +161 -0
  26. package/lib/searchReplace/index.d.ts +25 -0
  27. package/lib/searchReplace/index.js +68 -0
  28. package/lib/showOutput/index.d.ts +17 -0
  29. package/lib/showOutput/index.js +154 -0
  30. package/lib/terminals/index.d.ts +4 -0
  31. package/lib/terminals/index.js +65 -16
  32. package/lib/terminals/model.d.ts +56 -3
  33. package/lib/terminals/model.js +261 -5
  34. package/lib/terminals/widget.d.ts +3 -1
  35. package/lib/terminals/widget.js +11 -3
  36. package/lib/walkthrough/index.d.ts +14 -0
  37. package/lib/walkthrough/index.js +162 -0
  38. package/lib/walkthrough/panel.d.ts +54 -0
  39. package/lib/walkthrough/panel.js +98 -0
  40. package/package.json +1 -1
  41. package/schema/launcher.json +3 -2
  42. package/schema/terminals.json +15 -0
  43. package/src/commandBar/index.ts +165 -0
  44. package/src/fileBrowser/fileBrowser.tsx +8 -1
  45. package/src/highlight/index.ts +251 -0
  46. package/src/index.ts +13 -1
  47. package/src/launcher/agents.ts +25 -0
  48. package/src/launcher/editorRegistry.ts +8 -4
  49. package/src/launcher/editors.ts +18 -0
  50. package/src/launcher/index.ts +4 -0
  51. package/src/launcher/schemaDefaults.ts +53 -0
  52. package/src/omnibox/files.ts +68 -0
  53. package/src/omnibox/index.ts +105 -0
  54. package/src/omnibox/model.ts +210 -0
  55. package/src/omnibox/tokens.ts +29 -0
  56. package/src/omnibox/widget.tsx +322 -0
  57. package/src/searchReplace/index.ts +86 -0
  58. package/src/showOutput/index.ts +199 -0
  59. package/src/terminals/index.ts +79 -16
  60. package/src/terminals/model.ts +304 -3
  61. package/src/terminals/widget.tsx +21 -4
  62. package/src/walkthrough/index.ts +202 -0
  63. package/src/walkthrough/panel.ts +152 -0
  64. package/style/commandBar.css +104 -0
  65. package/style/highlight.css +11 -0
  66. package/style/index.css +5 -0
  67. package/style/index.js +5 -0
  68. package/style/omnibox.css +120 -0
  69. package/style/showOutput.css +11 -0
  70. package/style/terminals.css +54 -10
  71. package/style/walkthrough.css +80 -0
@@ -0,0 +1,98 @@
1
+ import { fileIcon } from '@jupyterlab/ui-components';
2
+ import { Panel, Widget } from '@lumino/widgets';
3
+ /** Command the jump links dispatch; keeps the panel decoupled from the plugin. */
4
+ const HIGHLIGHT_LINES_COMMAND = 'xtralab:highlight-lines';
5
+ /**
6
+ * A read-only, scrollable column of walkthrough steps for the side area.
7
+ *
8
+ * The agent appends steps as it narrates, so the explanation lives beside the
9
+ * code the user is looking at and persists for them to read at their own pace,
10
+ * rather than scrolling away in the chat. Each step renders Markdown (with code
11
+ * snippets, Mermaid, math), can embed a rich visual, and can offer a button
12
+ * that opens the referenced file and highlights its lines.
13
+ */
14
+ export class WalkthroughPanel extends Panel {
15
+ constructor(options) {
16
+ super();
17
+ this._rendermime = options.rendermime;
18
+ this._commands = options.commands;
19
+ this._trans = options.trans;
20
+ this.addClass('jp-xtralab-Walkthrough');
21
+ }
22
+ /** Remove every step, leaving an empty panel. */
23
+ clear() {
24
+ while (this.widgets.length > 0) {
25
+ this.widgets[0].dispose();
26
+ }
27
+ }
28
+ /**
29
+ * Dispose on close (e.g. the close button when the panel has been dragged to
30
+ * the main area) rather than leaving a detached, reusable widget behind. The
31
+ * next `xtralab:walkthrough` call recreates the panel in the side area.
32
+ */
33
+ onCloseRequest(msg) {
34
+ super.onCloseRequest(msg);
35
+ this.dispose();
36
+ }
37
+ /** Append a step and scroll it into view. */
38
+ async addStep(step) {
39
+ const card = new Panel();
40
+ card.addClass('jp-xtralab-Walkthrough-step');
41
+ this.addWidget(card);
42
+ if (step.title) {
43
+ const heading = new Widget({ node: document.createElement('h3') });
44
+ heading.addClass('jp-xtralab-Walkthrough-stepTitle');
45
+ heading.node.textContent = step.title;
46
+ card.addWidget(heading);
47
+ }
48
+ if (step.body) {
49
+ await this._render(card, 'text/markdown', step.body);
50
+ }
51
+ if (step.media && typeof step.media.mimeType === 'string') {
52
+ await this._render(card, step.media.mimeType, step.media.data);
53
+ }
54
+ if (step.path) {
55
+ card.addWidget(this._jumpButton(step.path, step.line, step.endLine));
56
+ }
57
+ // Bring the newest step into view.
58
+ this.node.scrollTop = this.node.scrollHeight;
59
+ }
60
+ async _render(parent, mimeType, data) {
61
+ const bundle = { [mimeType]: data };
62
+ const chosen = this._rendermime.preferredMimeType(bundle, 'any');
63
+ if (!chosen) {
64
+ return;
65
+ }
66
+ const renderer = this._rendermime.createRenderer(chosen);
67
+ renderer.addClass('jp-xtralab-Walkthrough-content');
68
+ parent.addWidget(renderer);
69
+ await renderer.renderModel(this._rendermime.createModel({ data: bundle, trusted: true }));
70
+ }
71
+ _jumpButton(path, line, endLine) {
72
+ const node = document.createElement('div');
73
+ node.className = 'jp-xtralab-Walkthrough-jumpRow';
74
+ const where = line ? `${path}:${line}` : path;
75
+ const button = document.createElement('button');
76
+ button.type = 'button';
77
+ button.className = 'jp-xtralab-Walkthrough-jump';
78
+ button.title = this._trans.__('Open %1 in the editor', where);
79
+ const icon = fileIcon.element({
80
+ tag: 'span',
81
+ className: 'jp-xtralab-Walkthrough-jumpIcon'
82
+ });
83
+ const label = document.createElement('span');
84
+ label.className = 'jp-xtralab-Walkthrough-jumpLabel';
85
+ label.textContent = where;
86
+ button.appendChild(icon);
87
+ button.appendChild(label);
88
+ node.appendChild(button);
89
+ button.addEventListener('click', () => {
90
+ void this._commands.execute(HIGHLIGHT_LINES_COMMAND, {
91
+ path,
92
+ line,
93
+ endLine
94
+ });
95
+ });
96
+ return new Widget({ node });
97
+ }
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtralab",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -2,12 +2,13 @@
2
2
  "title": "xtralab launcher",
3
3
  "description": "Settings for the xtralab agent launcher.",
4
4
  "jupyter.lab.shortcuts": [],
5
+ "jupyter.lab.transform": true,
5
6
  "type": "object",
6
7
  "additionalProperties": false,
7
8
  "properties": {
8
9
  "agents": {
9
10
  "title": "Agents",
10
- "description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, antigravity, copilot, goose, opencode, kiro, mistral-vibe), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Set `requireAvailable: false` for shell aliases that should always show even though they aren't on PATH.",
11
+ "description": "Override or extend the built-in list of agents shown on the launcher. Each entry must include an `id`. When the id matches a built-in (claude, codex, antigravity, copilot, goose, opencode, kiro, mistral-vibe), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define brand-new cards. Set `enabled: false` to hide a default. Set `requireAvailable: false` for shell aliases that should always show even though they aren't on PATH. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place.",
11
12
  "type": "array",
12
13
  "default": [],
13
14
  "items": {
@@ -68,7 +69,7 @@
68
69
  },
69
70
  "editors": {
70
71
  "title": "Editors",
71
- "description": "Override or extend the terminal editors offered in the launcher's Open section (and recognized in the Terminals panel). Each entry must include an `id`. When the id matches a built-in (nvim, vim), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define additional editors. The launcher shows a single tile — the first editor, by `rank`, whose `command` is on PATH (Neovim before Vim by default). Set `enabled: false` to hide a built-in; disable both to remove the tile entirely. Set `requireAvailable: false` for a shell alias that should show even though it isn't on PATH.",
72
+ "description": "Override or extend the terminal editors offered in the launcher's Open section (and recognized in the Terminals panel). Each entry must include an `id`. When the id matches a built-in (nvim, vim), the fields you set override the corresponding defaults; leave a field out to keep the default. New ids define additional editors. The launcher shows a single tile — the first editor, by `rank`, whose `command` is on PATH (Neovim before Vim by default). Set `enabled: false` to hide a built-in; disable both to remove the tile entirely. Set `requireAvailable: false` for a shell alias that should show even though it isn't on PATH. The full built-in list is shown as the default below, so you can copy an entry and tweak it in place.",
72
73
  "type": "array",
73
74
  "default": [],
74
75
  "items": {
@@ -0,0 +1,15 @@
1
+ {
2
+ "title": "xtralab Terminals panel",
3
+ "description": "The left-sidebar panel that lists running terminal sessions.",
4
+ "jupyter.lab.shortcuts": [],
5
+ "type": "object",
6
+ "properties": {
7
+ "showAgentActivity": {
8
+ "type": "boolean",
9
+ "title": "Show agent activity under each terminal",
10
+ "description": "When true, every terminal running a coding agent shows a smaller line below its title with the agent's latest line of output, so you can see what a backgrounded agent is doing without switching to it. Turn this off to list terminals by title alone.",
11
+ "default": true
12
+ }
13
+ },
14
+ "additionalProperties": false
15
+ }
@@ -0,0 +1,165 @@
1
+ import {
2
+ ILabShell,
3
+ JupyterFrontEnd,
4
+ JupyterFrontEndPlugin
5
+ } from '@jupyterlab/application';
6
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
7
+ import { searchIcon } from '@jupyterlab/ui-components';
8
+ import { CommandRegistry } from '@lumino/commands';
9
+ import { Widget } from '@lumino/widgets';
10
+
11
+ import { IOmnibox, OMNIBOX_OPEN_COMMAND } from '../omnibox/tokens';
12
+
13
+ const PLUGIN_ID = 'xtralab:command-bar';
14
+
15
+ /**
16
+ * A search-bar-styled launcher for the `top` area. It holds no text input of
17
+ * its own: clicking the pill (or pressing Enter/Space while it is focused)
18
+ * runs `onActivate`, which opens the omnibox.
19
+ *
20
+ * The widget's root is a transparent, full-height wrapper centered as an
21
+ * overlay over the whole top panel (style/commandBar.css). The wrapper
22
+ * ignores pointer events so only the inner pill is clickable and the empty
23
+ * strips above and below it let clicks fall through to whatever sits beneath
24
+ * the overlay, such as the menu bar.
25
+ */
26
+ class CommandBar extends Widget {
27
+ constructor(options: CommandBar.IOptions) {
28
+ super({
29
+ node: CommandBar.createNode(
30
+ options.label,
31
+ options.caption,
32
+ options.shortcut
33
+ )
34
+ });
35
+ this.addClass('jp-xtralab-CommandBar');
36
+ this._onActivate = options.onActivate;
37
+ }
38
+
39
+ handleEvent(event: Event): void {
40
+ if (event.type === 'click') {
41
+ this._onActivate();
42
+ }
43
+ }
44
+
45
+ protected onAfterAttach(): void {
46
+ // The pill restores `pointer-events: auto`, so the click bubbles from it
47
+ // up to the root node listened on here; keyboard Enter/Space on the
48
+ // focused button raises the same synthetic click.
49
+ this.node.addEventListener('click', this);
50
+ }
51
+
52
+ protected onBeforeDetach(): void {
53
+ this.node.removeEventListener('click', this);
54
+ }
55
+
56
+ private _onActivate: () => void;
57
+ }
58
+
59
+ namespace CommandBar {
60
+ export interface IOptions {
61
+ /** Placeholder-style text shown inside the pill. */
62
+ label: string;
63
+ /** Tooltip and accessible name for the button. */
64
+ caption: string;
65
+ /** Formatted keyboard shortcut shown as a trailing hint, if any. */
66
+ shortcut?: string;
67
+ /** Runs on click or keyboard activation. */
68
+ onActivate: () => void;
69
+ }
70
+
71
+ export function createNode(
72
+ label: string,
73
+ caption: string,
74
+ shortcut?: string
75
+ ): HTMLElement {
76
+ const wrapper = document.createElement('div');
77
+
78
+ const button = document.createElement('button');
79
+ button.type = 'button';
80
+ button.className = 'jp-xtralab-CommandBar-button';
81
+ button.title = caption;
82
+ button.setAttribute('aria-label', caption);
83
+
84
+ const icon = searchIcon.element({
85
+ tag: 'span',
86
+ className: 'jp-xtralab-CommandBar-icon'
87
+ });
88
+
89
+ const text = document.createElement('span');
90
+ text.className = 'jp-xtralab-CommandBar-label';
91
+ text.textContent = label;
92
+
93
+ button.appendChild(icon);
94
+ button.appendChild(text);
95
+
96
+ if (shortcut) {
97
+ const hint = document.createElement('span');
98
+ hint.className = 'jp-xtralab-CommandBar-shortcut';
99
+ hint.textContent = shortcut;
100
+ // Decorative: the button's aria-label already names the action.
101
+ hint.setAttribute('aria-hidden', 'true');
102
+ button.appendChild(hint);
103
+ }
104
+
105
+ wrapper.appendChild(button);
106
+ return wrapper;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Add a centered, search-bar-styled command bar to the top area. Clicking it
112
+ * opens the omnibox — a launcher overlay that fuzzy-searches files and
113
+ * commands and routes a typed prompt to an agent (the leading sidebar toggle
114
+ * and the menu bar sit to its left, the trailing sidebar toggle to its right;
115
+ * the bar is centered over the whole panel as an overlay, so it stays put
116
+ * regardless of their widths).
117
+ *
118
+ * The bar is only added when the omnibox is available — the `IOmnibox` token
119
+ * is provided by `xtralab:omnibox` — so it never appears with nothing to open.
120
+ */
121
+ const plugin: JupyterFrontEndPlugin<void> = {
122
+ id: PLUGIN_ID,
123
+ description:
124
+ 'Add a centered command bar to the top area that opens the omnibox.',
125
+ autoStart: true,
126
+ requires: [ILabShell],
127
+ optional: [IOmnibox, ITranslator],
128
+ activate: (
129
+ app: JupyterFrontEnd,
130
+ labShell: ILabShell,
131
+ omnibox: IOmnibox | null,
132
+ translator: ITranslator | null
133
+ ): void => {
134
+ if (!omnibox) {
135
+ return;
136
+ }
137
+
138
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
139
+
140
+ // Show the omnibox's keyboard shortcut as a hint in the pill, derived from
141
+ // the live binding (registered by xtralab:omnibox, which activates first as
142
+ // the IOmnibox provider) so it stays correct and uses the platform's
143
+ // modifier symbols. Empty when no binding is registered.
144
+ const binding = app.commands.keyBindings.find(
145
+ keyBinding => keyBinding.command === OMNIBOX_OPEN_COMMAND
146
+ );
147
+ const shortcut = binding
148
+ ? CommandRegistry.formatKeystroke(binding.keys)
149
+ : '';
150
+
151
+ const widget = new CommandBar({
152
+ label: trans.__('Search…'),
153
+ caption: trans.__('Search files and commands, or ask an agent'),
154
+ shortcut,
155
+ onActivate: () => {
156
+ omnibox.open();
157
+ }
158
+ });
159
+ widget.id = 'xtralab-command-bar';
160
+
161
+ labShell.add(widget, 'top', { rank: 500 });
162
+ }
163
+ };
164
+
165
+ export default plugin;
@@ -93,6 +93,12 @@ const GIT_REPO_PATH = '';
93
93
  */
94
94
  const FILE_TREE_TAG = 'file-tree-container';
95
95
 
96
+ const FILE_TREE_UNSAFE_CSS =
97
+ '[data-type="item"][data-item-selected="true"] ' +
98
+ '[data-item-section="spacing-item"] {' +
99
+ 'border-left-color: transparent;' +
100
+ '}';
101
+
96
102
  export interface IFileBrowserProps {
97
103
  contentsManager: Contents.IManager;
98
104
  docManager: IDocumentManager;
@@ -123,7 +129,8 @@ export function FileBrowserComponent(
123
129
  initialExpansion: 'closed',
124
130
  search: true,
125
131
  icons: FILE_BROWSER_ICONS,
126
- itemHeight: 24
132
+ itemHeight: 24,
133
+ unsafeCSS: FILE_TREE_UNSAFE_CSS
127
134
  });
128
135
 
129
136
  React.useEffect(() => {
@@ -0,0 +1,251 @@
1
+ import {
2
+ JupyterFrontEnd,
3
+ JupyterFrontEndPlugin
4
+ } from '@jupyterlab/application';
5
+ import { ICommandPalette } from '@jupyterlab/apputils';
6
+ import { CodeMirrorEditor } from '@jupyterlab/codemirror';
7
+ import { IDocumentManager } from '@jupyterlab/docmanager';
8
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
9
+ import { StateEffect, StateField } from '@codemirror/state';
10
+ import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
11
+ import type { Widget } from '@lumino/widgets';
12
+
13
+ const PLUGIN_ID = 'xtralab:highlight';
14
+
15
+ /**
16
+ * Highlight a contiguous range of lines, or clear every highlight. These are
17
+ * the only two commands the plugin contributes; both are designed to be driven
18
+ * by a coding agent over the MCP command bridge to walk a user through code.
19
+ */
20
+ const HIGHLIGHT_LINES_COMMAND = 'xtralab:highlight-lines';
21
+ const CLEAR_HIGHLIGHTS_COMMAND = 'xtralab:clear-highlights';
22
+
23
+ /** CodeMirror line-decoration class; styled in style/highlight.css. */
24
+ const HIGHLIGHT_LINE_CLASS = 'jp-xtralab-highlightLine';
25
+
26
+ /**
27
+ * A CodeMirror effect carrying the 1-indexed, inclusive line range to
28
+ * highlight, or `null` to clear the editor's highlights.
29
+ */
30
+ const setHighlight = StateEffect.define<{
31
+ start: number;
32
+ end: number;
33
+ } | null>();
34
+
35
+ /**
36
+ * Holds the highlight decorations for one editor. It is injected lazily (the
37
+ * first time an editor is highlighted) via `StateEffect.appendConfig`, so
38
+ * editors that are never highlighted pay nothing.
39
+ */
40
+ const highlightField = StateField.define<DecorationSet>({
41
+ create: () => Decoration.none,
42
+ update(decorations, transaction) {
43
+ // Keep decorations anchored as the user edits around them.
44
+ decorations = decorations.map(transaction.changes);
45
+ for (const effect of transaction.effects) {
46
+ if (!effect.is(setHighlight)) {
47
+ continue;
48
+ }
49
+ if (effect.value === null) {
50
+ decorations = Decoration.none;
51
+ continue;
52
+ }
53
+ const { doc } = transaction.state;
54
+ const lineDecoration = Decoration.line({ class: HIGHLIGHT_LINE_CLASS });
55
+ const start = Math.max(1, Math.min(effect.value.start, doc.lines));
56
+ const end = Math.max(start, Math.min(effect.value.end, doc.lines));
57
+ const ranges = [];
58
+ for (let line = start; line <= end; line++) {
59
+ ranges.push(lineDecoration.range(doc.line(line).from));
60
+ }
61
+ decorations = Decoration.set(ranges);
62
+ }
63
+ return decorations;
64
+ },
65
+ provide: field => EditorView.decorations.from(field)
66
+ });
67
+
68
+ /**
69
+ * Return the CodeMirror view backing a widget's editor, or `null` when the
70
+ * widget is not a CodeMirror-based text editor (a notebook, a terminal, the
71
+ * settings editor, …). Duck-typed on `content.editor` so the plugin does not
72
+ * depend on `@jupyterlab/fileeditor`.
73
+ */
74
+ function viewForWidget(widget: Widget | null): EditorView | null {
75
+ const editor = (widget as { content?: { editor?: unknown } } | null)?.content
76
+ ?.editor;
77
+ return editor instanceof CodeMirrorEditor ? editor.editor : null;
78
+ }
79
+
80
+ /**
81
+ * Coerce a JSON command argument to a positive integer line number, falling
82
+ * back to `fallback` when it is missing or not a finite number.
83
+ */
84
+ function toLine(value: unknown, fallback: number): number {
85
+ const n = Math.trunc(Number(value));
86
+ return Number.isFinite(n) && n >= 1 ? n : fallback;
87
+ }
88
+
89
+ /**
90
+ * Contribute `xtralab:highlight-lines` and `xtralab:clear-highlights`.
91
+ *
92
+ * The built-in command surface can open a file (`docmanager:open`) and move
93
+ * the cursor to a line (`fileeditor:go-to-line`), but it cannot persistently
94
+ * highlight a span of lines — `documentsearch:start` only marks text matches
95
+ * and hijacks the find box. This plugin fills that gap with a CodeMirror line
96
+ * decoration, so an agent can point at "lines 31–43 of src/index.ts" while it
97
+ * narrates a walkthrough. `xtralab:highlight-lines` opens the file when needed,
98
+ * scrolls the range into view, and replaces any previous highlight in that
99
+ * editor; `xtralab:clear-highlights` removes every highlight.
100
+ */
101
+ const plugin: JupyterFrontEndPlugin<void> = {
102
+ id: PLUGIN_ID,
103
+ description: 'Highlight line ranges in text editors to guide walkthroughs.',
104
+ autoStart: true,
105
+ requires: [IDocumentManager],
106
+ optional: [ICommandPalette, ITranslator],
107
+ activate: (
108
+ app: JupyterFrontEnd,
109
+ docManager: IDocumentManager,
110
+ palette: ICommandPalette | null,
111
+ translator: ITranslator | null
112
+ ): void => {
113
+ const { commands, shell } = app;
114
+ const trans = (translator ?? nullTranslator).load('jupyterlab');
115
+
116
+ // Editors that currently carry a highlight, so a single clear can reach
117
+ // them all. Disposed editors are dropped on the next clear.
118
+ const highlighted = new Set<EditorView>();
119
+
120
+ const ensureField = (view: EditorView): void => {
121
+ if (view.state.field(highlightField, false) === undefined) {
122
+ view.dispatch({ effects: StateEffect.appendConfig.of(highlightField) });
123
+ }
124
+ };
125
+
126
+ commands.addCommand(HIGHLIGHT_LINES_COMMAND, {
127
+ label: trans.__('Highlight Lines'),
128
+ caption: trans.__('Highlight a range of lines in a text editor'),
129
+ // Advertise the argument shape so agents listing commands over the MCP
130
+ // bridge can see how to call it, the same way core commands do.
131
+ describedBy: {
132
+ args: {
133
+ type: 'object',
134
+ properties: {
135
+ path: {
136
+ type: 'string',
137
+ description:
138
+ 'Path of the file to highlight in, relative to the server root. Opened if needed; defaults to the active editor when omitted.'
139
+ },
140
+ line: {
141
+ type: 'number',
142
+ description: 'First line to highlight (1-indexed). Defaults to 1.'
143
+ },
144
+ endLine: {
145
+ type: 'number',
146
+ description:
147
+ 'Last line to highlight (1-indexed, inclusive). Defaults to `line` for a single line.'
148
+ },
149
+ reveal: {
150
+ type: 'boolean',
151
+ description:
152
+ 'Whether to scroll the highlighted range into view. Defaults to true.'
153
+ }
154
+ }
155
+ }
156
+ },
157
+ execute: async args => {
158
+ const line = toLine(args['line'], 1);
159
+ const endLine = toLine(args['endLine'], line);
160
+ const path =
161
+ typeof args['path'] === 'string' ? args['path'] : undefined;
162
+ const reveal = args['reveal'] !== false;
163
+
164
+ // With a path, bind strictly to that document: never fall back to the
165
+ // active widget, or an explicit path could silently highlight an
166
+ // unrelated editor that happens to be focused.
167
+ let widget: Widget | null;
168
+ if (path) {
169
+ // Fail fast on a missing path instead of opening a phantom widget
170
+ // behind a blocking "file not found" dialog.
171
+ try {
172
+ await docManager.services.contents.get(path, { content: false });
173
+ } catch {
174
+ throw new Error(`xtralab: no file at "${path}"`);
175
+ }
176
+ const opened = docManager.openOrReveal(path);
177
+ if (!opened) {
178
+ throw new Error(`xtralab: could not open "${path}"`);
179
+ }
180
+ // Wait for the model and the editor view to be ready.
181
+ await opened.context.ready;
182
+ await opened.revealed;
183
+ widget = opened;
184
+ } else {
185
+ widget = shell.currentWidget;
186
+ }
187
+
188
+ const view = viewForWidget(widget);
189
+ if (!view) {
190
+ throw new Error(
191
+ path
192
+ ? `xtralab: "${path}" is not open in a text editor`
193
+ : 'xtralab: the active widget is not a text editor'
194
+ );
195
+ }
196
+
197
+ const start = Math.min(line, endLine);
198
+ const end = Math.max(line, endLine);
199
+ ensureField(view);
200
+
201
+ const lineCount = view.state.doc.lines;
202
+ const anchor = view.state.doc.line(
203
+ Math.max(1, Math.min(start, lineCount))
204
+ ).from;
205
+ const effects: StateEffect<unknown>[] = [
206
+ setHighlight.of({ start, end })
207
+ ];
208
+ if (reveal) {
209
+ effects.push(EditorView.scrollIntoView(anchor, { y: 'center' }));
210
+ }
211
+ view.dispatch({ effects });
212
+ // Forget editors that have since been closed before tracking this one.
213
+ for (const tracked of highlighted) {
214
+ if (!tracked.dom.isConnected) {
215
+ highlighted.delete(tracked);
216
+ }
217
+ }
218
+ highlighted.add(view);
219
+
220
+ return start === end
221
+ ? trans.__('Highlighted line %1', start)
222
+ : trans.__('Highlighted lines %1-%2', start, end);
223
+ }
224
+ });
225
+
226
+ commands.addCommand(CLEAR_HIGHLIGHTS_COMMAND, {
227
+ label: trans.__('Clear Highlights'),
228
+ caption: trans.__('Remove all xtralab line highlights'),
229
+ execute: () => {
230
+ for (const view of highlighted) {
231
+ try {
232
+ if (view.state.field(highlightField, false) !== undefined) {
233
+ view.dispatch({ effects: setHighlight.of(null) });
234
+ }
235
+ } catch {
236
+ // The editor was disposed; nothing to clear.
237
+ }
238
+ }
239
+ highlighted.clear();
240
+ }
241
+ });
242
+
243
+ if (palette) {
244
+ const category = trans.__('Other');
245
+ palette.addItem({ command: HIGHLIGHT_LINES_COMMAND, category });
246
+ palette.addItem({ command: CLEAR_HIGHLIGHTS_COMMAND, category });
247
+ }
248
+ }
249
+ };
250
+
251
+ export default plugin;
package/src/index.ts CHANGED
@@ -1,17 +1,23 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
 
3
3
  import agentSessionsPlugin from './agentSessions';
4
+ import commandBarPlugin from './commandBar';
4
5
  import fileBrowserPlugin from './fileBrowser';
5
6
  import fileTypeIconsPlugin from './fileTypeIcons';
6
7
  import editorBreadcrumbsPlugin from './editorBreadcrumbs';
7
8
  import editorIndentPlugin from './editorIndent';
8
9
  import gitPlugins from './git';
10
+ import highlightPlugin from './highlight';
9
11
  import launcherPlugins from './launcher';
10
12
  import menusPlugin from './menus';
13
+ import omniboxPlugin from './omnibox';
14
+ import searchReplacePlugin from './searchReplace';
15
+ import showOutputPlugin from './showOutput';
11
16
  import sidebarPlugin from './sidebar';
12
17
  import terminalNotificationsPlugin from './terminalNotifications';
13
18
  import terminalsPlugin from './terminals';
14
19
  import topBarPlugin from './topBar';
20
+ import walkthroughPlugin from './walkthrough';
15
21
 
16
22
  /**
17
23
  * Every plugin contributed by `xtralab`. The entry point of the
@@ -24,17 +30,23 @@ import topBarPlugin from './topBar';
24
30
  */
25
31
  const plugins: JupyterFrontEndPlugin<unknown>[] = [
26
32
  agentSessionsPlugin,
33
+ commandBarPlugin,
27
34
  editorBreadcrumbsPlugin,
28
35
  editorIndentPlugin,
29
36
  fileBrowserPlugin,
30
37
  fileTypeIconsPlugin,
31
38
  ...gitPlugins,
39
+ highlightPlugin,
32
40
  ...launcherPlugins,
33
41
  menusPlugin,
42
+ omniboxPlugin,
43
+ searchReplacePlugin,
44
+ showOutputPlugin,
34
45
  sidebarPlugin,
35
46
  terminalNotificationsPlugin,
36
47
  terminalsPlugin,
37
- topBarPlugin
48
+ topBarPlugin,
49
+ walkthroughPlugin
38
50
  ];
39
51
 
40
52
  export default plugins;
@@ -178,6 +178,31 @@ const DEFAULTS: IAgent[] = [
178
178
  }
179
179
  ];
180
180
 
181
+ /**
182
+ * The built-in agents projected into the JSON settings shape
183
+ * ({@link IAgentSettings}), dropping the runtime-only {@link LabIcon}.
184
+ * {@link registerLauncherSchemaDefaults} injects this as the `agents` setting's
185
+ * schema default so the Settings Editor shows the shipped list.
186
+ */
187
+ export function defaultAgentSettings(): IAgentSettings[] {
188
+ return DEFAULTS.map(agent => {
189
+ const entry: IAgentSettings = {
190
+ id: agent.id,
191
+ label: agent.label,
192
+ caption: agent.caption,
193
+ command: agent.command,
194
+ rank: agent.rank,
195
+ requireAvailable: agent.requireAvailable
196
+ };
197
+ // Omit `promptArgs` for agents that don't take an inline prompt, so the
198
+ // default doesn't show a prompt form they lack.
199
+ if (agent.promptArgs !== undefined) {
200
+ entry.promptArgs = agent.promptArgs;
201
+ }
202
+ return entry;
203
+ });
204
+ }
205
+
181
206
  function resolveIcon(id: string, iconSvg: string | undefined): LabIcon {
182
207
  if (iconSvg) {
183
208
  return new LabIcon({ name: `xtralab:agent-custom-${id}`, svgstr: iconSvg });