xtralab 0.5.0 → 0.7.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 +138 -76
- package/lib/agentSessions.d.ts +50 -0
- package/lib/agentSessions.js +45 -0
- package/lib/fileBrowser/index.js +5 -1
- package/lib/git/imageDiff.js +80 -8
- package/lib/index.d.ts +3 -2
- package/lib/index.js +11 -6
- package/lib/launcher/agents.d.ts +1 -1
- package/lib/launcher/agents.js +22 -13
- package/lib/launcher/commands.d.ts +32 -6
- package/lib/launcher/commands.js +81 -57
- package/lib/launcher/dashboard.d.ts +15 -0
- package/lib/launcher/dashboard.js +139 -10
- package/lib/launcher/editorRegistry.d.ts +43 -0
- package/lib/launcher/editorRegistry.js +96 -0
- package/lib/launcher/editors.d.ts +78 -0
- package/lib/launcher/editors.js +110 -0
- package/lib/launcher/icons.d.ts +25 -6
- package/lib/launcher/icons.js +116 -19
- package/lib/launcher/index.d.ts +2 -22
- package/lib/launcher/index.js +48 -25
- package/lib/launcher/registry.d.ts +21 -0
- package/lib/launcher/registry.js +28 -0
- package/lib/launcher/tokens.d.ts +41 -0
- package/lib/launcher/tokens.js +17 -0
- package/lib/terminals/detection.d.ts +11 -0
- package/lib/terminals/detection.js +50 -0
- package/lib/terminals/index.d.ts +27 -0
- package/lib/terminals/index.js +194 -0
- package/lib/terminals/model.d.ts +191 -0
- package/lib/terminals/model.js +418 -0
- package/lib/terminals/widget.d.ts +72 -0
- package/lib/terminals/widget.js +93 -0
- package/lib/topBar/icons.d.ts +13 -0
- package/lib/topBar/icons.js +27 -0
- package/lib/topBar/index.d.ts +16 -0
- package/lib/topBar/index.js +98 -0
- package/package.json +36 -33
- package/schema/launcher.json +56 -1
- package/src/agentSessions.ts +94 -0
- package/src/fileBrowser/index.ts +5 -1
- package/src/git/imageDiff.tsx +127 -26
- package/src/index.ts +11 -6
- package/src/launcher/agents.ts +23 -14
- package/src/launcher/commands.ts +98 -60
- package/src/launcher/dashboard.tsx +227 -9
- package/src/launcher/editorRegistry.ts +149 -0
- package/src/launcher/editors.ts +183 -0
- package/src/launcher/icons.ts +120 -20
- package/src/launcher/index.ts +62 -27
- package/src/launcher/registry.ts +33 -0
- package/src/launcher/tokens.ts +51 -0
- package/src/terminals/detection.ts +65 -0
- package/src/terminals/index.ts +222 -0
- package/src/terminals/model.ts +497 -0
- package/src/terminals/widget.tsx +240 -0
- package/src/topBar/icons.ts +30 -0
- package/src/topBar/index.ts +134 -0
- package/style/git.css +109 -0
- package/style/index.css +2 -1
- package/style/index.js +2 -1
- package/style/launcher.css +124 -2
- package/style/terminals.css +235 -0
- package/style/topBar.css +38 -0
- package/lib/statusBar/index.d.ts +0 -14
- package/lib/statusBar/index.js +0 -89
- package/lib/statusBar/widget.d.ts +0 -121
- package/lib/statusBar/widget.js +0 -237
- package/src/statusBar/index.ts +0 -115
- package/src/statusBar/widget.tsx +0 -318
- package/style/statusBar.css +0 -95
package/lib/launcher/commands.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { DisposableSet } from '@lumino/disposable';
|
|
2
2
|
import { buildAgentInvocation } from './invocation';
|
|
3
|
+
import { agentCommandId } from './tokens';
|
|
3
4
|
/**
|
|
4
5
|
* The command id for opening the launcher.
|
|
5
6
|
*/
|
|
6
7
|
export const CREATE_LAUNCHER_COMMAND = 'launcher:create';
|
|
7
|
-
/**
|
|
8
|
-
* Build a JupyterLab command id for a given agent. Centralised so the
|
|
9
|
-
* plugin and the launcher both use the same names.
|
|
10
|
-
*/
|
|
11
|
-
export function agentCommandId(agentId) {
|
|
12
|
-
return `xtralab:start-agent:${agentId}`;
|
|
13
|
-
}
|
|
14
8
|
/**
|
|
15
9
|
* Register a JupyterLab command per agent. Each command opens a new
|
|
16
10
|
* terminal via `terminal:create-new` and feeds the agent's shell command
|
|
@@ -19,8 +13,13 @@ export function agentCommandId(agentId) {
|
|
|
19
13
|
* Returns a single disposable that tears down every registered command —
|
|
20
14
|
* the launcher disposes the previous set before re-registering on settings
|
|
21
15
|
* changes so the command palette stays in sync with the configured agents.
|
|
16
|
+
*
|
|
17
|
+
* When an `agentSessions` registry is supplied, each launch tags its terminal
|
|
18
|
+
* session with the agent's command, so the terminals panel can badge the row
|
|
19
|
+
* with the agent's logo immediately (before server-side detection confirms
|
|
20
|
+
* it).
|
|
22
21
|
*/
|
|
23
|
-
export function registerAgentCommands(app, agents) {
|
|
22
|
+
export function registerAgentCommands(app, agents, agentSessions = null) {
|
|
24
23
|
const disposables = new DisposableSet();
|
|
25
24
|
for (const agent of agents) {
|
|
26
25
|
const disposable = app.commands.addCommand(agentCommandId(agent.id), {
|
|
@@ -32,58 +31,83 @@ export function registerAgentCommands(app, agents) {
|
|
|
32
31
|
const promptArg = args['prompt'];
|
|
33
32
|
const prompt = typeof promptArg === 'string' ? promptArg : '';
|
|
34
33
|
const invocation = buildAgentInvocation(agent, prompt);
|
|
35
|
-
|
|
36
|
-
cwd
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const sendCommand = () => {
|
|
45
|
-
session.send({
|
|
46
|
-
type: 'stdin',
|
|
47
|
-
content: [invocation + '\r']
|
|
48
|
-
});
|
|
49
|
-
// Give the tab and the status-bar list a meaningful default
|
|
50
|
-
// label. The XTerm widget's `_initialConnection` listener
|
|
51
|
-
// overwrites the title with `Terminal {N}` when the WebSocket
|
|
52
|
-
// first reports `connected`; we connect *after* that listener
|
|
53
|
-
// (and call `applyAgentLabel` after `sendCommand`), so by the
|
|
54
|
-
// time we run the upstream listener has already had its turn.
|
|
55
|
-
// Programs that publish a real xterm title escape sequence
|
|
56
|
-
// still win — `XTerm.onTitleChange` resets the label whenever
|
|
57
|
-
// it fires.
|
|
58
|
-
applyAgentLabel();
|
|
59
|
-
};
|
|
60
|
-
const applyAgentLabel = () => {
|
|
61
|
-
if (main.isDisposed || main.content.isDisposed) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
main.content.title.label = agent.label;
|
|
65
|
-
};
|
|
66
|
-
// Match how the Terminal widget itself sends its `initialCommand`:
|
|
67
|
-
// either fire immediately if the session is already connected, or
|
|
68
|
-
// wait for the next `connectionStatusChanged` that flips it to
|
|
69
|
-
// `connected`. Without this the stdin write races the websocket
|
|
70
|
-
// handshake and the agent command silently disappears.
|
|
71
|
-
if (session.connectionStatus === 'connected') {
|
|
72
|
-
sendCommand();
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
const onStatus = () => {
|
|
76
|
-
if (session.connectionStatus === 'connected') {
|
|
77
|
-
session.connectionStatusChanged.disconnect(onStatus);
|
|
78
|
-
sendCommand();
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
session.connectionStatusChanged.connect(onStatus);
|
|
82
|
-
}
|
|
83
|
-
return main;
|
|
34
|
+
return launchInTerminal(app.commands, {
|
|
35
|
+
cwd,
|
|
36
|
+
invocation,
|
|
37
|
+
label: agent.label,
|
|
38
|
+
// Optimistically tag the session with this agent so the terminals
|
|
39
|
+
// panel can show its logo right away; server-side detection takes
|
|
40
|
+
// over as the source of truth on its next poll.
|
|
41
|
+
onSession: name => agentSessions === null || agentSessions === void 0 ? void 0 : agentSessions.set(name, agent.command)
|
|
42
|
+
});
|
|
84
43
|
}
|
|
85
44
|
});
|
|
86
45
|
disposables.add(disposable);
|
|
87
46
|
}
|
|
88
47
|
return disposables;
|
|
89
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Open a fresh terminal, type `invocation` into it as if the user had, and
|
|
51
|
+
* label the tab. Shared by the per-agent launch commands and the launcher's
|
|
52
|
+
* editor tile so the fiddly "wait for the websocket, then write stdin"
|
|
53
|
+
* sequence lives in one place.
|
|
54
|
+
*
|
|
55
|
+
* `onSession`, when given, is called once with the new session's name right
|
|
56
|
+
* after the terminal is revealed. Callers use it to optimistically tag the
|
|
57
|
+
* session so the terminals panel can badge it with the agent's (or editor's)
|
|
58
|
+
* logo before server-side detection confirms the running process.
|
|
59
|
+
*
|
|
60
|
+
* Resolves with the host `MainAreaWidget` so the caller can place it — the
|
|
61
|
+
* launcher swaps it into its own tab.
|
|
62
|
+
*/
|
|
63
|
+
export async function launchInTerminal(commands, options) {
|
|
64
|
+
const { cwd, invocation, label, onSession } = options;
|
|
65
|
+
const main = (await commands.execute('terminal:create-new', {
|
|
66
|
+
cwd
|
|
67
|
+
}));
|
|
68
|
+
// `MainAreaWidget.revealed` chains off the `reveal` promise the terminal
|
|
69
|
+
// extension passes in (the xterm.js widget's own `ready` promise), so
|
|
70
|
+
// awaiting it guarantees the Terminal widget has finished its constructor
|
|
71
|
+
// and connected its session listeners.
|
|
72
|
+
await main.revealed;
|
|
73
|
+
const session = main.content.session;
|
|
74
|
+
onSession === null || onSession === void 0 ? void 0 : onSession(session.name);
|
|
75
|
+
const applyLabel = () => {
|
|
76
|
+
if (main.isDisposed || main.content.isDisposed) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
main.content.title.label = label;
|
|
80
|
+
};
|
|
81
|
+
const sendCommand = () => {
|
|
82
|
+
session.send({
|
|
83
|
+
type: 'stdin',
|
|
84
|
+
content: [invocation + '\r']
|
|
85
|
+
});
|
|
86
|
+
// Give the tab and the status-bar list a meaningful default label. The
|
|
87
|
+
// XTerm widget's `_initialConnection` listener overwrites the title with
|
|
88
|
+
// `Terminal {N}` when the WebSocket first reports `connected`; we connect
|
|
89
|
+
// *after* that listener (and call `applyLabel` after `sendCommand`), so by
|
|
90
|
+
// the time we run the upstream listener has already had its turn. Programs
|
|
91
|
+
// that publish a real xterm title escape sequence still win —
|
|
92
|
+
// `XTerm.onTitleChange` resets the label whenever it fires.
|
|
93
|
+
applyLabel();
|
|
94
|
+
};
|
|
95
|
+
// Match how the Terminal widget itself sends its `initialCommand`: either
|
|
96
|
+
// fire immediately if the session is already connected, or wait for the next
|
|
97
|
+
// `connectionStatusChanged` that flips it to `connected`. Without this the
|
|
98
|
+
// stdin write races the websocket handshake and the command silently
|
|
99
|
+
// disappears.
|
|
100
|
+
if (session.connectionStatus === 'connected') {
|
|
101
|
+
sendCommand();
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const onStatus = () => {
|
|
105
|
+
if (session.connectionStatus === 'connected') {
|
|
106
|
+
session.connectionStatusChanged.disconnect(onStatus);
|
|
107
|
+
sendCommand();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
session.connectionStatusChanged.connect(onStatus);
|
|
111
|
+
}
|
|
112
|
+
return main;
|
|
113
|
+
}
|
|
@@ -2,7 +2,9 @@ import * as React from 'react';
|
|
|
2
2
|
import type { CommandRegistry } from '@lumino/commands';
|
|
3
3
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
4
4
|
import type { Widget } from '@lumino/widgets';
|
|
5
|
+
import type { IAgentSessions } from '../agentSessions';
|
|
5
6
|
import type { IAgent } from './agents';
|
|
7
|
+
import type { IEditor } from './editors';
|
|
6
8
|
/**
|
|
7
9
|
* Public construction options for the launcher dashboard widget. The
|
|
8
10
|
* plugin supplies an `onAgentLaunch` callback that performs the same
|
|
@@ -17,6 +19,19 @@ export interface ILauncherDashboardOptions {
|
|
|
17
19
|
* rank; the widget renders them as-is.
|
|
18
20
|
*/
|
|
19
21
|
agents: IAgent[];
|
|
22
|
+
/**
|
|
23
|
+
* The terminal editor to offer in the "Open" section (Neovim preferred over
|
|
24
|
+
* Vim), or `null` when neither is installed. Resolved by the plugin from the
|
|
25
|
+
* same availability probe that filters the agents.
|
|
26
|
+
*/
|
|
27
|
+
editor: IEditor | null;
|
|
28
|
+
/**
|
|
29
|
+
* Shared launch-tag registry. When the editor tile launches, it records the
|
|
30
|
+
* session here so the terminals panel badges it with the editor's logo right
|
|
31
|
+
* away, the same as agent launches do. `null` when the provider plugin is
|
|
32
|
+
* unavailable.
|
|
33
|
+
*/
|
|
34
|
+
agentSessions: IAgentSessions | null;
|
|
20
35
|
/**
|
|
21
36
|
* Called after an agent command has returned its top-level widget. The
|
|
22
37
|
* plugin uses this to swap the new terminal into the launcher's tab.
|
|
@@ -3,7 +3,8 @@ import { Poll } from '@lumino/polling';
|
|
|
3
3
|
import { LabIcon, ReactWidget, consoleIcon, notebookIcon, refreshIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import { expandStatusFiles, status } from '../git/api';
|
|
5
5
|
import { CommandIDs as GitCommandIDs } from '../git/commands';
|
|
6
|
-
import {
|
|
6
|
+
import { launchInTerminal } from './commands';
|
|
7
|
+
import { agentCommandId } from './tokens';
|
|
7
8
|
/**
|
|
8
9
|
* Lumino host for the React-based launcher dashboard. Mirrors the
|
|
9
10
|
* `ReactWidget` pattern used by the git panel so the rest of the plugin
|
|
@@ -32,7 +33,7 @@ const CHANGES_POLL_INTERVAL_MS = 8000;
|
|
|
32
33
|
*/
|
|
33
34
|
const CHANGES_POLL_MAX_MS = 300000;
|
|
34
35
|
function LauncherDashboardComponent(props) {
|
|
35
|
-
const { commands, agents, onAgentLaunch, repoPath, cwd } = props;
|
|
36
|
+
const { commands, agents, editor, agentSessions, onAgentLaunch, repoPath, cwd } = props;
|
|
36
37
|
const [prompt, setPrompt] = React.useState('');
|
|
37
38
|
const [git, setGit] = React.useState({
|
|
38
39
|
loading: true,
|
|
@@ -90,13 +91,28 @@ function LauncherDashboardComponent(props) {
|
|
|
90
91
|
}
|
|
91
92
|
}, [commands, cwd, onAgentLaunch]);
|
|
92
93
|
const launchNotebook = React.useCallback(async () => {
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// Open a fresh notebook with no kernel attached — equivalent to clicking
|
|
95
|
+
// "No Kernel" in the Select Kernel dialog. `notebook:create-new` always
|
|
96
|
+
// routes through that dialog when no kernelName is given, so drive the
|
|
97
|
+
// underlying docmanager steps directly and pass `shouldStart: false` to
|
|
98
|
+
// suppress kernel selection. The user picks a kernel from the notebook
|
|
99
|
+
// toolbar when they're ready.
|
|
100
|
+
const file = (await commands.execute('docmanager:new-untitled', {
|
|
101
|
+
path: cwd,
|
|
102
|
+
type: 'notebook'
|
|
103
|
+
}));
|
|
104
|
+
if (!file) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const result = (await commands.execute('docmanager:open', {
|
|
108
|
+
path: file.path,
|
|
109
|
+
factory: 'Notebook',
|
|
110
|
+
kernelPreference: { shouldStart: false, canStart: true }
|
|
98
111
|
}));
|
|
99
112
|
if (result) {
|
|
113
|
+
// Match `notebook:create-new`'s post-open marker so the first manual
|
|
114
|
+
// save offers a rename instead of overwriting the auto-generated name.
|
|
115
|
+
result.isUntitled = true;
|
|
100
116
|
onAgentLaunch(result);
|
|
101
117
|
}
|
|
102
118
|
}, [commands, cwd, onAgentLaunch]);
|
|
@@ -108,13 +124,30 @@ function LauncherDashboardComponent(props) {
|
|
|
108
124
|
onAgentLaunch(result);
|
|
109
125
|
}
|
|
110
126
|
}, [commands, cwd, onAgentLaunch]);
|
|
127
|
+
const launchEditor = React.useCallback(async () => {
|
|
128
|
+
if (!editor) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// An editor is "open a terminal and type its command" — the agent launch
|
|
132
|
+
// minus the prompt — so it shares `launchInTerminal` rather than the native
|
|
133
|
+
// `terminal:create-new` call the tiles above use, and tags the session the
|
|
134
|
+
// same way so the terminals panel badges it with the editor's logo.
|
|
135
|
+
const result = await launchInTerminal(commands, {
|
|
136
|
+
cwd,
|
|
137
|
+
invocation: editor.command,
|
|
138
|
+
label: editor.label,
|
|
139
|
+
onSession: name => agentSessions === null || agentSessions === void 0 ? void 0 : agentSessions.set(name, editor.command)
|
|
140
|
+
});
|
|
141
|
+
onAgentLaunch(result);
|
|
142
|
+
}, [commands, cwd, editor, agentSessions, onAgentLaunch]);
|
|
111
143
|
const openDiff = React.useCallback((change) => {
|
|
112
144
|
const args = { repoPath, change };
|
|
113
145
|
void commands.execute(GitCommandIDs.openDiff, args);
|
|
114
146
|
}, [commands, repoPath]);
|
|
115
147
|
return (React.createElement("div", { className: "jp-xtralab-Launcher-body" },
|
|
116
148
|
React.createElement(AgentSection, { agents: agents, prompt: prompt, onPromptChange: setPrompt, onLaunch: launch }),
|
|
117
|
-
React.createElement(
|
|
149
|
+
React.createElement(McpSection, { agents: agents }),
|
|
150
|
+
React.createElement(OpenSection, { editor: editor, onLaunchTerminal: launchTerminal, onLaunchNotebook: launchNotebook, onLaunchConsole: launchConsole, onLaunchEditor: launchEditor }),
|
|
118
151
|
React.createElement(ChangesSection, { git: git, onOpen: openDiff, onRefresh: () => void refresh() })));
|
|
119
152
|
}
|
|
120
153
|
function ChangesSection(props) {
|
|
@@ -202,15 +235,108 @@ function AgentSection(props) {
|
|
|
202
235
|
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, agent.label)));
|
|
203
236
|
}))));
|
|
204
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Launcher agents whose CLI has a clean, non-interactive `mcp add` that takes
|
|
240
|
+
* `<name> -- <command>`. The other agents (Goose, OpenCode, Kiro, Mistral
|
|
241
|
+
* Vibe, Antigravity) register MCP through their own config files or UI, so
|
|
242
|
+
* there is no one-line command to surface for them.
|
|
243
|
+
*/
|
|
244
|
+
const MCP_ADD_AGENT_IDS = new Set(['claude', 'codex', 'copilot']);
|
|
245
|
+
/** The stdio proxy console script that bridges an agent to the MCP server. */
|
|
246
|
+
const MCP_PROXY_COMMAND = 'jupyter-server-mcp-proxy';
|
|
247
|
+
/**
|
|
248
|
+
* Copy `text` to the clipboard via a hidden, selected `<textarea>` and the
|
|
249
|
+
* legacy `execCommand('copy')`. Deprecated, but still the most reliable path
|
|
250
|
+
* under a user gesture when the async Clipboard API is unavailable or blocked
|
|
251
|
+
* (the desktop app's Electron window can deny clipboard writes by policy).
|
|
252
|
+
* Returns whether the copy succeeded.
|
|
253
|
+
*/
|
|
254
|
+
function legacyCopy(text) {
|
|
255
|
+
try {
|
|
256
|
+
const textarea = document.createElement('textarea');
|
|
257
|
+
textarea.value = text;
|
|
258
|
+
textarea.setAttribute('readonly', '');
|
|
259
|
+
textarea.style.position = 'fixed';
|
|
260
|
+
textarea.style.top = '-1000px';
|
|
261
|
+
textarea.style.opacity = '0';
|
|
262
|
+
document.body.appendChild(textarea);
|
|
263
|
+
textarea.select();
|
|
264
|
+
const ok = document.execCommand('copy');
|
|
265
|
+
document.body.removeChild(textarea);
|
|
266
|
+
return ok;
|
|
267
|
+
}
|
|
268
|
+
catch (_a) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A collapsed-by-default hint listing the one-line command that registers
|
|
274
|
+
* xtralab's running Jupyter MCP server with each installed agent that
|
|
275
|
+
* supports `mcp add`. Copy-only: the user runs it in their own terminal,
|
|
276
|
+
* where the server is reachable with no port to configure — the desktop
|
|
277
|
+
* supervisor exports `JUPYTER_SERVER_MCP_URL`, and `pip` installs let the
|
|
278
|
+
* proxy discover the server from its runtime file. Renders nothing when none
|
|
279
|
+
* of those agents are on `$PATH`.
|
|
280
|
+
*/
|
|
281
|
+
function McpSection(props) {
|
|
282
|
+
const [copiedId, setCopiedId] = React.useState(null);
|
|
283
|
+
const registerable = props.agents.filter(agent => MCP_ADD_AGENT_IDS.has(agent.id));
|
|
284
|
+
if (registerable.length === 0) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const flagCopied = (id) => {
|
|
288
|
+
setCopiedId(id);
|
|
289
|
+
window.setTimeout(() => setCopiedId(current => (current === id ? null : current)), 1500);
|
|
290
|
+
};
|
|
291
|
+
const copy = (command, id) => {
|
|
292
|
+
// Prefer the async Clipboard API; fall back to `legacyCopy` when it's
|
|
293
|
+
// missing or rejects (Electron can block it by policy). Flag "Copied" only
|
|
294
|
+
// once a copy actually succeeds.
|
|
295
|
+
const clipboard = navigator.clipboard;
|
|
296
|
+
if (clipboard) {
|
|
297
|
+
void clipboard.writeText(command).then(() => flagCopied(id), () => {
|
|
298
|
+
if (legacyCopy(command)) {
|
|
299
|
+
flagCopied(id);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
else if (legacyCopy(command)) {
|
|
304
|
+
flagCopied(id);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return (React.createElement("details", { className: "jp-xtralab-Launcher-section jp-xtralab-Launcher-mcp-section" },
|
|
308
|
+
React.createElement("summary", { className: "jp-xtralab-Launcher-section-summary" },
|
|
309
|
+
React.createElement("span", { className: "jp-xtralab-Launcher-section-title" }, "Connect agents to JupyterLab (MCP)")),
|
|
310
|
+
React.createElement("div", { className: "jp-xtralab-Launcher-section-body" },
|
|
311
|
+
React.createElement("p", { className: "jp-xtralab-Launcher-agent-hint" }, "Run once per agent to let it drive this JupyterLab over MCP. No port needed \u2014 the proxy finds this server on its own."),
|
|
312
|
+
React.createElement("ul", { className: "jp-xtralab-Launcher-mcp-list" }, registerable.map(agent => {
|
|
313
|
+
const command = `${agent.command} mcp add jupyter -- ${MCP_PROXY_COMMAND}`;
|
|
314
|
+
return (React.createElement("li", { key: agent.id, className: "jp-xtralab-Launcher-mcp-item" },
|
|
315
|
+
React.createElement("code", { className: "jp-xtralab-Launcher-mcp-command" }, command),
|
|
316
|
+
React.createElement("button", { type: "button", className: copiedId === agent.id
|
|
317
|
+
? 'jp-xtralab-Launcher-mcp-copy jp-mod-copied'
|
|
318
|
+
: 'jp-xtralab-Launcher-mcp-copy', "aria-label": `Copy the ${agent.label} command`, onClick: () => copy(command, agent.id) }, copiedId === agent.id ? 'Copied!' : 'Copy')));
|
|
319
|
+
})),
|
|
320
|
+
React.createElement("p", { className: "jp-xtralab-Launcher-mcp-note" },
|
|
321
|
+
"Other agents (Goose, OpenCode, Kiro, Mistral Vibe, Antigravity) register MCP through their own config \u2014 point them at the same",
|
|
322
|
+
' ',
|
|
323
|
+
React.createElement("code", null, MCP_PROXY_COMMAND),
|
|
324
|
+
" command."))));
|
|
325
|
+
}
|
|
205
326
|
/**
|
|
206
327
|
* The non-agent launch tiles — a plain terminal, a new notebook, and a
|
|
207
328
|
* new console. They sit in a separate section below the agent grid so
|
|
208
329
|
* they don't compete with the AI agents visually, and they're always
|
|
209
330
|
* available regardless of the agent prompt textarea (which doesn't
|
|
210
331
|
* apply to them).
|
|
332
|
+
*
|
|
333
|
+
* A terminal editor (Neovim, else Vim) joins the row when one is installed —
|
|
334
|
+
* it opens in a terminal like the agents do, so it lives here rather than in
|
|
335
|
+
* the agent grid. When neither is on `$PATH`, `editor` is `null` and no tile
|
|
336
|
+
* renders.
|
|
211
337
|
*/
|
|
212
338
|
function OpenSection(props) {
|
|
213
|
-
const { onLaunchTerminal, onLaunchNotebook, onLaunchConsole } = props;
|
|
339
|
+
const { editor, onLaunchTerminal, onLaunchNotebook, onLaunchConsole, onLaunchEditor } = props;
|
|
214
340
|
return (React.createElement("section", { className: "jp-xtralab-Launcher-section" },
|
|
215
341
|
React.createElement("h2", { className: "jp-xtralab-Launcher-section-title" }, "Open"),
|
|
216
342
|
React.createElement("div", { className: "jp-xtralab-Launcher-agents" },
|
|
@@ -222,5 +348,8 @@ function OpenSection(props) {
|
|
|
222
348
|
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Notebook")),
|
|
223
349
|
React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: "Create a new console.", "aria-label": "Create a new console", onClick: onLaunchConsole },
|
|
224
350
|
React.createElement(LabIcon.resolveReact, { icon: consoleIcon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
|
|
225
|
-
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Console"))
|
|
351
|
+
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, "Console")),
|
|
352
|
+
editor && (React.createElement("button", { type: "button", className: "jp-xtralab-Launcher-agent", title: editor.caption, "aria-label": editor.caption, onClick: onLaunchEditor },
|
|
353
|
+
React.createElement(LabIcon.resolveReact, { icon: editor.icon, tag: "span", className: "jp-xtralab-Launcher-agent-icon" }),
|
|
354
|
+
React.createElement("span", { className: "jp-xtralab-Launcher-agent-label" }, editor.label))))));
|
|
226
355
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
import { Token } from '@lumino/coreutils';
|
|
3
|
+
import { ISignal } from '@lumino/signaling';
|
|
4
|
+
import { type IEditor } from './editors';
|
|
5
|
+
/**
|
|
6
|
+
* A read-only, observable view of the launcher's terminal editors: xtralab's
|
|
7
|
+
* built-ins (Neovim, Vim) merged with the user's `editors` setting, sorted by
|
|
8
|
+
* rank, with disabled entries removed.
|
|
9
|
+
*/
|
|
10
|
+
export interface IEditorRegistry {
|
|
11
|
+
/**
|
|
12
|
+
* The full merged editor list (disabled entries already removed). The
|
|
13
|
+
* terminals panel uses it to badge a row for any configured editor that is
|
|
14
|
+
* running, and to tell the server which editor commands to detect.
|
|
15
|
+
*/
|
|
16
|
+
readonly editors: IEditor[];
|
|
17
|
+
/**
|
|
18
|
+
* The single editor tile the launcher shows: the first editor, by rank, whose
|
|
19
|
+
* command is on `$PATH` (Neovim before Vim by default), or `null` when none
|
|
20
|
+
* qualifies.
|
|
21
|
+
*/
|
|
22
|
+
readonly current: IEditor | null;
|
|
23
|
+
/**
|
|
24
|
+
* Emitted whenever {@link editors} or {@link current} changes (the user
|
|
25
|
+
* edited the `editors` setting). Consumers that read on demand can ignore it.
|
|
26
|
+
*/
|
|
27
|
+
readonly changed: ISignal<IEditorRegistry, void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* DI token for {@link IEditorRegistry}. Provided by `xtralab:editor-registry`
|
|
31
|
+
* and consumed — optionally — by `xtralab:launcher` (for its tile) and
|
|
32
|
+
* `xtralab:terminals` (to badge running editors with their logo).
|
|
33
|
+
*/
|
|
34
|
+
export declare const IEditorRegistry: Token<IEditorRegistry>;
|
|
35
|
+
/**
|
|
36
|
+
* Provides {@link IEditorRegistry}: merges xtralab's built-in editors with the
|
|
37
|
+
* user's `editors` setting, probes the server's `$PATH` to resolve the launcher
|
|
38
|
+
* tile, and re-runs both on a settings change.
|
|
39
|
+
*
|
|
40
|
+
* `activate` awaits the first probe before returning, so a consumer reading
|
|
41
|
+
* {@link IEditorRegistry.current} on activation sees a resolved tile.
|
|
42
|
+
*/
|
|
43
|
+
export declare const editorRegistryPlugin: JupyterFrontEndPlugin<IEditorRegistry>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
2
|
+
import { Token } from '@lumino/coreutils';
|
|
3
|
+
import { Signal } from '@lumino/signaling';
|
|
4
|
+
import { fetchAvailableCommands } from './availability';
|
|
5
|
+
import { mergeEditors, resolveEditor } from './editors';
|
|
6
|
+
/**
|
|
7
|
+
* DI token for {@link IEditorRegistry}. Provided by `xtralab:editor-registry`
|
|
8
|
+
* and consumed — optionally — by `xtralab:launcher` (for its tile) and
|
|
9
|
+
* `xtralab:terminals` (to badge running editors with their logo).
|
|
10
|
+
*/
|
|
11
|
+
export const IEditorRegistry = new Token('xtralab:IEditorRegistry', 'A read-only, observable view of the launcher terminal editors, shared so the launcher tile and the terminals panel agree on the list and icons.');
|
|
12
|
+
/**
|
|
13
|
+
* Concrete {@link IEditorRegistry}. The provider plugin is the only writer, so
|
|
14
|
+
* the write side ({@link set}) is kept off the shared token.
|
|
15
|
+
*/
|
|
16
|
+
class EditorRegistry {
|
|
17
|
+
constructor() {
|
|
18
|
+
this._editors = [];
|
|
19
|
+
this._current = null;
|
|
20
|
+
this._changed = new Signal(this);
|
|
21
|
+
}
|
|
22
|
+
get editors() {
|
|
23
|
+
return this._editors;
|
|
24
|
+
}
|
|
25
|
+
get current() {
|
|
26
|
+
return this._current;
|
|
27
|
+
}
|
|
28
|
+
get changed() {
|
|
29
|
+
return this._changed;
|
|
30
|
+
}
|
|
31
|
+
/** Replace both lists (recomputed together) and notify observers. */
|
|
32
|
+
set(editors, current) {
|
|
33
|
+
this._editors = editors;
|
|
34
|
+
this._current = current;
|
|
35
|
+
this._changed.emit();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Schema id the `editors` setting lives under (shared with the launcher). */
|
|
39
|
+
const LAUNCHER_SETTINGS_ID = 'xtralab:launcher';
|
|
40
|
+
/**
|
|
41
|
+
* Provides {@link IEditorRegistry}: merges xtralab's built-in editors with the
|
|
42
|
+
* user's `editors` setting, probes the server's `$PATH` to resolve the launcher
|
|
43
|
+
* tile, and re-runs both on a settings change.
|
|
44
|
+
*
|
|
45
|
+
* `activate` awaits the first probe before returning, so a consumer reading
|
|
46
|
+
* {@link IEditorRegistry.current} on activation sees a resolved tile.
|
|
47
|
+
*/
|
|
48
|
+
export const editorRegistryPlugin = {
|
|
49
|
+
id: 'xtralab:editor-registry',
|
|
50
|
+
description: "Provides the launcher's terminal-editor list (Neovim, Vim, or user-configured), resolved against the server PATH.",
|
|
51
|
+
autoStart: true,
|
|
52
|
+
provides: IEditorRegistry,
|
|
53
|
+
optional: [ISettingRegistry],
|
|
54
|
+
activate: async (app, settingRegistry) => {
|
|
55
|
+
const registry = new EditorRegistry();
|
|
56
|
+
const apply = async (overrides) => {
|
|
57
|
+
const editors = mergeEditors(overrides);
|
|
58
|
+
// Probe only the editors that opt into the availability check; entries
|
|
59
|
+
// with `requireAvailable: false` (e.g. a shell alias) are shown
|
|
60
|
+
// regardless and don't need a `which` lookup.
|
|
61
|
+
const probe = editors
|
|
62
|
+
.filter(editor => editor.requireAvailable)
|
|
63
|
+
.map(editor => editor.command);
|
|
64
|
+
const available = await fetchAvailableCommands(probe);
|
|
65
|
+
registry.set(editors, resolveEditor(editors, available));
|
|
66
|
+
};
|
|
67
|
+
const readOverrides = (settings) => {
|
|
68
|
+
const raw = settings.composite.editors;
|
|
69
|
+
return Array.isArray(raw) ? raw : [];
|
|
70
|
+
};
|
|
71
|
+
if (settingRegistry) {
|
|
72
|
+
try {
|
|
73
|
+
const settings = await settingRegistry.load(LAUNCHER_SETTINGS_ID);
|
|
74
|
+
await apply(readOverrides(settings));
|
|
75
|
+
settings.changed.connect(async () => {
|
|
76
|
+
try {
|
|
77
|
+
await apply(readOverrides(settings));
|
|
78
|
+
}
|
|
79
|
+
catch (reason) {
|
|
80
|
+
console.error('xtralab: failed to reapply editor settings', reason);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (reason) {
|
|
85
|
+
console.error('xtralab: failed to load editor settings', reason);
|
|
86
|
+
// Settings load failed — fall back to the built-in editors so the
|
|
87
|
+
// launcher tile still resolves instead of going silent.
|
|
88
|
+
await apply([]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
await apply([]);
|
|
93
|
+
}
|
|
94
|
+
return registry;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
/**
|
|
3
|
+
* A terminal text editor surfaced on the launcher's "Open" section. An editor
|
|
4
|
+
* takes no initial prompt and is launched by typing its command into a fresh
|
|
5
|
+
* terminal (e.g. `nvim`). The launcher shows a single editor tile (the first
|
|
6
|
+
* available candidate; see {@link resolveEditor}); the full list is shared with
|
|
7
|
+
* the terminals panel to badge running editors.
|
|
8
|
+
*/
|
|
9
|
+
export interface IEditor {
|
|
10
|
+
/** Stable id; also the default `$PATH` command probed for availability. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Tile label, e.g. "Neovim". */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Tile tooltip. */
|
|
15
|
+
caption: string;
|
|
16
|
+
/** The literal command typed into the new terminal, e.g. `nvim`. */
|
|
17
|
+
command: string;
|
|
18
|
+
/** Brand icon for the tile. */
|
|
19
|
+
icon: LabIcon;
|
|
20
|
+
/**
|
|
21
|
+
* Preference order for the single launcher tile: the first candidate, by
|
|
22
|
+
* ascending rank, that qualifies wins (Neovim before Vim by default).
|
|
23
|
+
*/
|
|
24
|
+
rank: number;
|
|
25
|
+
/**
|
|
26
|
+
* When false, the launcher skips the `which`-based availability check for
|
|
27
|
+
* this entry — useful for a shell alias or function that isn't on PATH but
|
|
28
|
+
* still resolves when typed in a real terminal. Defaults to true.
|
|
29
|
+
*/
|
|
30
|
+
requireAvailable: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The settings-side shape: every field except `id` is optional, so a user can
|
|
34
|
+
* override a single field on a built-in editor (e.g. swap `vim`'s command for
|
|
35
|
+
* a wrapper) without restating the whole entry. New ids define brand-new
|
|
36
|
+
* editors. Mirrors `IAgentSettings` minus `promptArgs`.
|
|
37
|
+
*/
|
|
38
|
+
export interface IEditorSettings {
|
|
39
|
+
id: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
caption?: string;
|
|
42
|
+
command?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Inline SVG for a custom editor's icon. Required for new ids whose icon
|
|
45
|
+
* isn't shipped with xtralab; ignored for built-in ids unless explicitly
|
|
46
|
+
* set (in which case it overrides the built-in).
|
|
47
|
+
*/
|
|
48
|
+
iconSvg?: string;
|
|
49
|
+
rank?: number;
|
|
50
|
+
/**
|
|
51
|
+
* When false, the editor is hidden from the launcher's Open section (and no
|
|
52
|
+
* longer recognised by the terminals panel). Disable both built-ins to drop
|
|
53
|
+
* the editor tile entirely. Defaults to true.
|
|
54
|
+
*/
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
/** See {@link IEditor.requireAvailable}. */
|
|
57
|
+
requireAvailable?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Merge xtralab's built-in editors with the user's `editors` settings. Built-in
|
|
61
|
+
* entries keep their fields unless explicitly overridden; user-only entries are
|
|
62
|
+
* appended. `enabled: false` filters an entry out of the result entirely (so
|
|
63
|
+
* callers don't need to re-check the flag). The result is sorted by rank, which
|
|
64
|
+
* is also the launcher's tile-preference order. Mirrors `mergeAgents`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function mergeEditors(overrides: IEditorSettings[]): IEditor[];
|
|
67
|
+
/**
|
|
68
|
+
* Pick the single editor tile to show, given the merged editor list and the
|
|
69
|
+
* set of commands known to be on `$PATH` (from the launcher's availability
|
|
70
|
+
* probe). Returns the first candidate, by rank, that is either available or
|
|
71
|
+
* opts out of the check (`requireAvailable: false`) — Neovim before Vim by
|
|
72
|
+
* default — or `null` when none qualifies.
|
|
73
|
+
*
|
|
74
|
+
* When `available` is `null` (the endpoint couldn't be reached), a
|
|
75
|
+
* `requireAvailable` editor is not shown; an entry with
|
|
76
|
+
* `requireAvailable: false` is shown regardless.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveEditor(editors: IEditor[], available: Set<string> | null): IEditor | null;
|