xtralab 0.1.2 → 0.3.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 +129 -46
- package/lib/fileBrowser/commands.d.ts +2 -2
- package/lib/fileBrowser/commands.js +2 -2
- package/lib/fileBrowser/fileBrowser.js +72 -2
- package/lib/fileBrowser/gitignore.d.ts +29 -0
- package/lib/fileBrowser/gitignore.js +74 -0
- package/lib/fileBrowser/icons.d.ts +4 -4
- package/lib/fileBrowser/icons.js +4 -4
- package/lib/fileBrowser/widget.d.ts +2 -2
- package/lib/fileBrowser/widget.js +2 -2
- package/lib/git/api.d.ts +11 -0
- package/lib/git/api.js +19 -0
- package/lib/git/commands.d.ts +3 -1
- package/lib/git/commands.js +10 -4
- package/lib/git/diffWidget.d.ts +11 -0
- package/lib/git/diffWidget.js +90 -9
- package/lib/git/icons.d.ts +1 -1
- package/lib/git/icons.js +1 -1
- package/lib/git/index.js +22 -6
- package/lib/git/panelComponent.d.ts +3 -1
- package/lib/git/panelComponent.js +6 -3
- package/lib/index.js +9 -1
- package/lib/launcher/agents.d.ts +78 -0
- package/lib/launcher/agents.js +168 -0
- package/lib/launcher/availability.d.ts +12 -0
- package/lib/launcher/availability.js +53 -0
- package/lib/launcher/commands.d.ts +22 -0
- package/lib/launcher/commands.js +89 -0
- package/lib/launcher/dashboard.d.ts +46 -0
- package/lib/launcher/dashboard.js +214 -0
- package/lib/launcher/icons.d.ts +33 -0
- package/lib/launcher/icons.js +113 -0
- package/lib/launcher/index.d.ts +23 -0
- package/lib/launcher/index.js +191 -0
- package/lib/launcher/invocation.d.ts +29 -0
- package/lib/launcher/invocation.js +42 -0
- package/lib/menus/index.d.ts +14 -0
- package/lib/menus/index.js +102 -0
- package/lib/sidebar/index.d.ts +18 -0
- package/lib/sidebar/index.js +155 -0
- package/lib/statusBar/index.d.ts +14 -0
- package/lib/statusBar/index.js +89 -0
- package/lib/statusBar/widget.d.ts +121 -0
- package/lib/statusBar/widget.js +237 -0
- package/package.json +25 -3
- package/schema/launcher.json +70 -0
- package/schema/sidebar.json +57 -0
- package/src/fileBrowser/commands.ts +2 -2
- package/src/fileBrowser/fileBrowser.tsx +76 -2
- package/src/fileBrowser/gitignore.ts +86 -0
- package/src/fileBrowser/icons.ts +4 -4
- package/src/fileBrowser/widget.tsx +3 -3
- package/src/git/api.ts +20 -0
- package/src/git/commands.ts +19 -5
- package/src/git/diffWidget.tsx +109 -13
- package/src/git/icons.ts +1 -1
- package/src/git/index.ts +31 -5
- package/src/git/panelComponent.tsx +17 -5
- package/src/index.ts +9 -1
- package/src/launcher/agents.ts +248 -0
- package/src/launcher/availability.ts +73 -0
- package/src/launcher/commands.ts +103 -0
- package/src/launcher/dashboard.tsx +462 -0
- package/src/launcher/icons.ts +123 -0
- package/src/launcher/index.ts +228 -0
- package/src/launcher/invocation.ts +45 -0
- package/src/menus/index.ts +124 -0
- package/src/sidebar/index.ts +195 -0
- package/src/statusBar/index.ts +115 -0
- package/src/statusBar/widget.tsx +318 -0
- package/style/git.css +4 -8
- package/style/index.css +2 -0
- package/style/index.js +2 -0
- package/style/launcher.css +323 -0
- package/style/statusBar.css +95 -0
|
@@ -21,7 +21,12 @@ import { FileChangeStatus, IFileChange, IGitStatusResult } from './tokens';
|
|
|
21
21
|
*/
|
|
22
22
|
export interface IGitPanelHandlers {
|
|
23
23
|
/** Open a diff viewer for the given file in the main area. */
|
|
24
|
-
openDiff(
|
|
24
|
+
openDiff(
|
|
25
|
+
change: IFileChange,
|
|
26
|
+
options?: {
|
|
27
|
+
pin?: boolean;
|
|
28
|
+
}
|
|
29
|
+
): void;
|
|
25
30
|
/** Show a confirmation dialog. Returns true when the user accepted. */
|
|
26
31
|
confirm(title: string, body: string, accept: string): Promise<boolean>;
|
|
27
32
|
/** Show an error dialog. */
|
|
@@ -281,7 +286,11 @@ export function GitPanelComponent(props: {
|
|
|
281
286
|
key={`${change.group}:${change.path}`}
|
|
282
287
|
change={change}
|
|
283
288
|
pendingAction={pendingActions[`${change.group}:${change.path}`]}
|
|
284
|
-
onClick={
|
|
289
|
+
onClick={event =>
|
|
290
|
+
handlers.openDiff(change, {
|
|
291
|
+
pin: event.ctrlKey || event.metaKey
|
|
292
|
+
})
|
|
293
|
+
}
|
|
285
294
|
actions={[
|
|
286
295
|
{
|
|
287
296
|
label: 'Unstage changes',
|
|
@@ -314,7 +323,11 @@ export function GitPanelComponent(props: {
|
|
|
314
323
|
key={`${change.group}:${change.path}`}
|
|
315
324
|
change={change}
|
|
316
325
|
pendingAction={pendingActions[`${change.group}:${change.path}`]}
|
|
317
|
-
onClick={
|
|
326
|
+
onClick={event =>
|
|
327
|
+
handlers.openDiff(change, {
|
|
328
|
+
pin: event.ctrlKey || event.metaKey
|
|
329
|
+
})
|
|
330
|
+
}
|
|
318
331
|
actions={[
|
|
319
332
|
{
|
|
320
333
|
label: 'Discard changes',
|
|
@@ -365,7 +378,6 @@ function PanelHeader(props: {
|
|
|
365
378
|
return (
|
|
366
379
|
<div className="jp-xtralab-GitPanel-header">
|
|
367
380
|
<div className="jp-xtralab-GitPanel-headerInfo">
|
|
368
|
-
<div className="jp-xtralab-GitPanel-title">Source Control</div>
|
|
369
381
|
{repoName !== null ? (
|
|
370
382
|
<div className="jp-xtralab-GitPanel-repo" title={topLevel ?? ''}>
|
|
371
383
|
{repoName}
|
|
@@ -442,7 +454,7 @@ function ChangeSection(props: {
|
|
|
442
454
|
function ChangeRow(props: {
|
|
443
455
|
change: IFileChange;
|
|
444
456
|
pendingAction: string | undefined;
|
|
445
|
-
onClick: () => void;
|
|
457
|
+
onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
446
458
|
actions: IRowAction[];
|
|
447
459
|
}): React.ReactElement {
|
|
448
460
|
const { change, pendingAction, onClick, actions } = props;
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
|
2
2
|
|
|
3
3
|
import fileBrowserPlugin from './fileBrowser';
|
|
4
4
|
import gitPlugin from './git';
|
|
5
|
+
import launcherPlugin from './launcher';
|
|
6
|
+
import menusPlugin from './menus';
|
|
7
|
+
import sidebarPlugin from './sidebar';
|
|
8
|
+
import statusBarPlugin from './statusBar';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Every plugin contributed by `xtralab`. The entry point of the
|
|
@@ -12,7 +16,11 @@ import gitPlugin from './git';
|
|
|
12
16
|
*/
|
|
13
17
|
const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
14
18
|
fileBrowserPlugin,
|
|
15
|
-
gitPlugin
|
|
19
|
+
gitPlugin,
|
|
20
|
+
launcherPlugin,
|
|
21
|
+
menusPlugin,
|
|
22
|
+
sidebarPlugin,
|
|
23
|
+
statusBarPlugin
|
|
16
24
|
];
|
|
17
25
|
|
|
18
26
|
export default plugins;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { LabIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
|
|
3
|
+
import { BUILTIN_AGENT_ICONS } from './icons';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shape of an agent card on the launcher. The `command` is the literal
|
|
7
|
+
* text typed into the new terminal session — interactive shells expand
|
|
8
|
+
* aliases, so users can point at `claude`, `cl`, or whatever runs their
|
|
9
|
+
* preferred wrapper.
|
|
10
|
+
*/
|
|
11
|
+
export interface IAgent {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
caption: string;
|
|
15
|
+
command: string;
|
|
16
|
+
icon: LabIcon;
|
|
17
|
+
rank: number;
|
|
18
|
+
/**
|
|
19
|
+
* When false, the launcher skips the `which`-based availability check for
|
|
20
|
+
* this entry — useful for aliases or shell functions that aren't on PATH
|
|
21
|
+
* but still resolve when typed in a real terminal. Defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
requireAvailable: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Argv tokens spliced between `command` and a shell-quoted prompt when
|
|
26
|
+
* the user types one into the launcher's prompt box. The semantics:
|
|
27
|
+
*
|
|
28
|
+
* - `[]` → prompt is appended as a positional argument:
|
|
29
|
+
* `<command> 'PROMPT'`. (Used by claude, codex, gemini, vibe.)
|
|
30
|
+
* - `['-i']` / `['--prompt']` → the prompt is preceded by a flag:
|
|
31
|
+
* `<command> -i 'PROMPT'`. (Used by copilot, opencode.)
|
|
32
|
+
* - `undefined` → the agent does not accept an initial prompt. The
|
|
33
|
+
* launcher dims the agent's button while the prompt textarea is
|
|
34
|
+
* non-empty, so the user gets a clear signal rather than a silently
|
|
35
|
+
* dropped prompt.
|
|
36
|
+
*
|
|
37
|
+
* The prompt itself is always single-quoted with embedded single quotes
|
|
38
|
+
* escaped, so multi-line prompts and shell metacharacters are safe.
|
|
39
|
+
*/
|
|
40
|
+
promptArgs?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The settings-side shape: every field except `id` is optional, so a user
|
|
45
|
+
* can override a single field on a default agent (e.g. swap the command for
|
|
46
|
+
* an alias) without restating the whole entry. New ids define brand-new
|
|
47
|
+
* agent cards.
|
|
48
|
+
*/
|
|
49
|
+
export interface IAgentSettings {
|
|
50
|
+
id: string;
|
|
51
|
+
label?: string;
|
|
52
|
+
caption?: string;
|
|
53
|
+
command?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Inline SVG for a custom agent's icon. Required for new ids whose icon
|
|
56
|
+
* isn't shipped with xtralab; ignored for default ids unless explicitly
|
|
57
|
+
* set (in which case it overrides the built-in).
|
|
58
|
+
*/
|
|
59
|
+
iconSvg?: string;
|
|
60
|
+
rank?: number;
|
|
61
|
+
/**
|
|
62
|
+
* When false, the agent is hidden from the launcher and the command
|
|
63
|
+
* palette. Defaults to true.
|
|
64
|
+
*/
|
|
65
|
+
enabled?: boolean;
|
|
66
|
+
/** See `IAgent.requireAvailable`. */
|
|
67
|
+
requireAvailable?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* See `IAgent.promptArgs`. Pass an empty array to mark the agent as
|
|
70
|
+
* accepting a positional prompt; pass an array like `["-p"]` to use a
|
|
71
|
+
* flag. Pass `null` to explicitly turn off prompt support for an agent
|
|
72
|
+
* that has it on by default.
|
|
73
|
+
*/
|
|
74
|
+
promptArgs?: string[] | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default agent cards shipped with xtralab. The list mirrors the eight
|
|
79
|
+
* first-class personas in `jupyter-ai-contrib/jupyter-ai-acp-client` (which
|
|
80
|
+
* is also where the icons come from), but uses the bare CLI names a user
|
|
81
|
+
* would type in a terminal rather than the ACP wrapper binaries that
|
|
82
|
+
* project spawns. Anything not on the user's `$PATH` is filtered out at
|
|
83
|
+
* activation time, so the wider list is harmless.
|
|
84
|
+
*/
|
|
85
|
+
const DEFAULTS: IAgent[] = [
|
|
86
|
+
{
|
|
87
|
+
id: 'claude',
|
|
88
|
+
label: 'Claude',
|
|
89
|
+
caption: 'Start Claude Code in a new terminal.',
|
|
90
|
+
command: 'claude',
|
|
91
|
+
icon: BUILTIN_AGENT_ICONS.claude,
|
|
92
|
+
rank: 0,
|
|
93
|
+
requireAvailable: true,
|
|
94
|
+
promptArgs: []
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'codex',
|
|
98
|
+
label: 'Codex',
|
|
99
|
+
caption: 'Start the Codex CLI in a new terminal.',
|
|
100
|
+
command: 'codex',
|
|
101
|
+
icon: BUILTIN_AGENT_ICONS.codex,
|
|
102
|
+
rank: 1,
|
|
103
|
+
requireAvailable: true,
|
|
104
|
+
promptArgs: []
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'gemini',
|
|
108
|
+
label: 'Gemini',
|
|
109
|
+
caption: 'Start the Gemini CLI in a new terminal.',
|
|
110
|
+
command: 'gemini',
|
|
111
|
+
icon: BUILTIN_AGENT_ICONS.gemini,
|
|
112
|
+
rank: 2,
|
|
113
|
+
requireAvailable: true,
|
|
114
|
+
promptArgs: []
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'copilot',
|
|
118
|
+
label: 'Copilot',
|
|
119
|
+
caption: 'Start the GitHub Copilot CLI in a new terminal.',
|
|
120
|
+
command: 'copilot',
|
|
121
|
+
icon: BUILTIN_AGENT_ICONS.copilot,
|
|
122
|
+
rank: 3,
|
|
123
|
+
requireAvailable: true,
|
|
124
|
+
// `-i "PROMPT"` is the documented way to start interactive mode and
|
|
125
|
+
// auto-execute the prompt; `-p` exits after responding (non-interactive).
|
|
126
|
+
promptArgs: ['-i']
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'goose',
|
|
130
|
+
label: 'Goose',
|
|
131
|
+
caption: 'Start Goose in a new terminal.',
|
|
132
|
+
command: 'goose',
|
|
133
|
+
icon: BUILTIN_AGENT_ICONS.goose,
|
|
134
|
+
rank: 4,
|
|
135
|
+
requireAvailable: true
|
|
136
|
+
// `goose` does not accept an inline interactive prompt; users would
|
|
137
|
+
// need to type the prompt after `goose session` starts.
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'opencode',
|
|
141
|
+
label: 'OpenCode',
|
|
142
|
+
caption: 'Start OpenCode in a new terminal.',
|
|
143
|
+
command: 'opencode',
|
|
144
|
+
icon: BUILTIN_AGENT_ICONS.opencode,
|
|
145
|
+
rank: 5,
|
|
146
|
+
requireAvailable: true,
|
|
147
|
+
promptArgs: ['--prompt']
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'kiro',
|
|
151
|
+
label: 'Kiro',
|
|
152
|
+
caption: 'Start the Kiro CLI in a new terminal.',
|
|
153
|
+
command: 'kiro',
|
|
154
|
+
icon: BUILTIN_AGENT_ICONS.kiro,
|
|
155
|
+
rank: 6,
|
|
156
|
+
requireAvailable: true
|
|
157
|
+
// The Kiro chat session only takes initial prompts via the in-session
|
|
158
|
+
// `/chat new <prompt>` slash command, not a CLI argument.
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'mistral-vibe',
|
|
162
|
+
label: 'Mistral Vibe',
|
|
163
|
+
caption: 'Start Mistral Vibe in a new terminal.',
|
|
164
|
+
command: 'vibe',
|
|
165
|
+
icon: BUILTIN_AGENT_ICONS['mistral-vibe'],
|
|
166
|
+
rank: 7,
|
|
167
|
+
requireAvailable: true,
|
|
168
|
+
promptArgs: []
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
function resolveIcon(id: string, iconSvg: string | undefined): LabIcon {
|
|
173
|
+
if (iconSvg) {
|
|
174
|
+
return new LabIcon({ name: `xtralab:agent-custom-${id}`, svgstr: iconSvg });
|
|
175
|
+
}
|
|
176
|
+
return BUILTIN_AGENT_ICONS[id] ?? terminalIcon;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Merge xtralab's defaults with the user's settings. Default entries keep
|
|
181
|
+
* their built-in fields unless explicitly overridden; user-only entries are
|
|
182
|
+
* appended. `enabled: false` filters an entry out of the result entirely
|
|
183
|
+
* (so callers don't need to check the flag again).
|
|
184
|
+
*/
|
|
185
|
+
export function mergeAgents(overrides: IAgentSettings[]): IAgent[] {
|
|
186
|
+
const overrideById = new Map(overrides.map(entry => [entry.id, entry]));
|
|
187
|
+
const merged: IAgent[] = [];
|
|
188
|
+
|
|
189
|
+
for (const base of DEFAULTS) {
|
|
190
|
+
const override = overrideById.get(base.id);
|
|
191
|
+
if (!override) {
|
|
192
|
+
merged.push({ ...base });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
overrideById.delete(base.id);
|
|
196
|
+
if (override.enabled === false) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
merged.push({
|
|
200
|
+
id: base.id,
|
|
201
|
+
label: override.label ?? base.label,
|
|
202
|
+
caption: override.caption ?? base.caption,
|
|
203
|
+
command: override.command ?? base.command,
|
|
204
|
+
icon: override.iconSvg
|
|
205
|
+
? resolveIcon(base.id, override.iconSvg)
|
|
206
|
+
: base.icon,
|
|
207
|
+
rank: override.rank ?? base.rank,
|
|
208
|
+
requireAvailable: override.requireAvailable ?? base.requireAvailable,
|
|
209
|
+
// `null` is the explicit way to opt out of an agent's default prompt
|
|
210
|
+
// support; an absent key keeps the default. `undefined` from `??` is
|
|
211
|
+
// pruned below.
|
|
212
|
+
promptArgs:
|
|
213
|
+
override.promptArgs === null
|
|
214
|
+
? undefined
|
|
215
|
+
: (override.promptArgs ?? base.promptArgs)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// What remains in `overrideById` are entries with ids that don't match a
|
|
220
|
+
// default — treat them as new agents the user is adding. Skip silently
|
|
221
|
+
// when a required field is missing rather than throwing; the settings
|
|
222
|
+
// schema validates the shape, so this is just a defensive fallback.
|
|
223
|
+
let nextRank =
|
|
224
|
+
merged.reduce((max, agent) => Math.max(max, agent.rank), -1) + 1;
|
|
225
|
+
for (const entry of overrideById.values()) {
|
|
226
|
+
if (entry.enabled === false) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const label = entry.label ?? entry.id;
|
|
230
|
+
const command = entry.command ?? entry.id;
|
|
231
|
+
merged.push({
|
|
232
|
+
id: entry.id,
|
|
233
|
+
label,
|
|
234
|
+
caption: entry.caption ?? `Start ${label} in a new terminal.`,
|
|
235
|
+
command,
|
|
236
|
+
icon: resolveIcon(entry.id, entry.iconSvg),
|
|
237
|
+
rank: entry.rank ?? nextRank++,
|
|
238
|
+
requireAvailable: entry.requireAvailable ?? true,
|
|
239
|
+
promptArgs:
|
|
240
|
+
entry.promptArgs === null || entry.promptArgs === undefined
|
|
241
|
+
? undefined
|
|
242
|
+
: entry.promptArgs
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
merged.sort((a, b) => a.rank - b.rank);
|
|
247
|
+
return merged;
|
|
248
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hit the xtralab server extension's `which`-proxy endpoint and turn the
|
|
6
|
+
* response into a Set of commands that resolved to a real binary on the
|
|
7
|
+
* server's `$PATH`. The frontend uses this set to filter the launcher's
|
|
8
|
+
* agent cards.
|
|
9
|
+
*
|
|
10
|
+
* On any failure (server extension not loaded, network error, malformed
|
|
11
|
+
* response) the returned Set is `null` — callers should treat that as
|
|
12
|
+
* "availability unknown" and either fall back to showing all agents or
|
|
13
|
+
* surface the error, depending on context.
|
|
14
|
+
*/
|
|
15
|
+
export async function fetchAvailableCommands(
|
|
16
|
+
commands: string[]
|
|
17
|
+
): Promise<Set<string> | null> {
|
|
18
|
+
if (commands.length === 0) {
|
|
19
|
+
return new Set();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const settings = ServerConnection.makeSettings();
|
|
23
|
+
const url = URLExt.join(
|
|
24
|
+
settings.baseUrl,
|
|
25
|
+
'xtralab',
|
|
26
|
+
'agents',
|
|
27
|
+
'availability'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
let response: Response;
|
|
31
|
+
try {
|
|
32
|
+
response = await ServerConnection.makeRequest(
|
|
33
|
+
url,
|
|
34
|
+
{
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: JSON.stringify({ commands })
|
|
37
|
+
},
|
|
38
|
+
settings
|
|
39
|
+
);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn('xtralab: availability check failed', error);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
console.warn(
|
|
47
|
+
`xtralab: availability endpoint returned HTTP ${response.status}`
|
|
48
|
+
);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let payload: unknown;
|
|
53
|
+
try {
|
|
54
|
+
payload = await response.json();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn('xtralab: availability response was not JSON', error);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!payload || typeof payload !== 'object') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const available = new Set<string>();
|
|
65
|
+
for (const [command, path] of Object.entries(
|
|
66
|
+
payload as Record<string, unknown>
|
|
67
|
+
)) {
|
|
68
|
+
if (typeof path === 'string' && path.length > 0) {
|
|
69
|
+
available.add(command);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return available;
|
|
73
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import type { MainAreaWidget } from '@jupyterlab/apputils';
|
|
3
|
+
import type { ITerminal } from '@jupyterlab/terminal';
|
|
4
|
+
import { DisposableSet, type IDisposable } from '@lumino/disposable';
|
|
5
|
+
|
|
6
|
+
import type { IAgent } from './agents';
|
|
7
|
+
import { buildAgentInvocation } from './invocation';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The command id for opening the launcher.
|
|
11
|
+
*/
|
|
12
|
+
export const CREATE_LAUNCHER_COMMAND = 'launcher:create';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a JupyterLab command id for a given agent. Centralised so the
|
|
16
|
+
* plugin and the launcher both use the same names.
|
|
17
|
+
*/
|
|
18
|
+
export function agentCommandId(agentId: string): string {
|
|
19
|
+
return `xtralab:start-agent:${agentId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a JupyterLab command per agent. Each command opens a new
|
|
24
|
+
* terminal via `terminal:create-new` and feeds the agent's shell command
|
|
25
|
+
* into the fresh session as if the user had typed it.
|
|
26
|
+
*
|
|
27
|
+
* Returns a single disposable that tears down every registered command —
|
|
28
|
+
* the launcher disposes the previous set before re-registering on settings
|
|
29
|
+
* changes so the command palette stays in sync with the configured agents.
|
|
30
|
+
*/
|
|
31
|
+
export function registerAgentCommands(
|
|
32
|
+
app: JupyterFrontEnd,
|
|
33
|
+
agents: IAgent[]
|
|
34
|
+
): IDisposable {
|
|
35
|
+
const disposables = new DisposableSet();
|
|
36
|
+
for (const agent of agents) {
|
|
37
|
+
const disposable = app.commands.addCommand(agentCommandId(agent.id), {
|
|
38
|
+
label: agent.label,
|
|
39
|
+
caption: agent.caption,
|
|
40
|
+
icon: agent.icon,
|
|
41
|
+
execute: async args => {
|
|
42
|
+
const cwd = args['cwd'] as string | undefined;
|
|
43
|
+
const promptArg = args['prompt'];
|
|
44
|
+
const prompt = typeof promptArg === 'string' ? promptArg : '';
|
|
45
|
+
const invocation = buildAgentInvocation(agent, prompt);
|
|
46
|
+
const main = (await app.commands.execute('terminal:create-new', {
|
|
47
|
+
cwd
|
|
48
|
+
})) as MainAreaWidget<ITerminal.ITerminal>;
|
|
49
|
+
// `MainAreaWidget.revealed` chains off the `reveal` promise the
|
|
50
|
+
// terminal extension passes in (the xterm.js widget's own `ready`
|
|
51
|
+
// promise), so awaiting it guarantees the Terminal widget has
|
|
52
|
+
// finished its constructor and connected its session listeners.
|
|
53
|
+
await main.revealed;
|
|
54
|
+
|
|
55
|
+
const session = main.content.session;
|
|
56
|
+
const sendCommand = (): void => {
|
|
57
|
+
session.send({
|
|
58
|
+
type: 'stdin',
|
|
59
|
+
content: [invocation + '\r']
|
|
60
|
+
});
|
|
61
|
+
// Give the tab and the status-bar list a meaningful default
|
|
62
|
+
// label. The XTerm widget's `_initialConnection` listener
|
|
63
|
+
// overwrites the title with `Terminal {N}` when the WebSocket
|
|
64
|
+
// first reports `connected`; we connect *after* that listener
|
|
65
|
+
// (and call `applyAgentLabel` after `sendCommand`), so by the
|
|
66
|
+
// time we run the upstream listener has already had its turn.
|
|
67
|
+
// Programs that publish a real xterm title escape sequence
|
|
68
|
+
// still win — `XTerm.onTitleChange` resets the label whenever
|
|
69
|
+
// it fires.
|
|
70
|
+
applyAgentLabel();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const applyAgentLabel = (): void => {
|
|
74
|
+
if (main.isDisposed || main.content.isDisposed) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
main.content.title.label = agent.label;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Match how the Terminal widget itself sends its `initialCommand`:
|
|
81
|
+
// either fire immediately if the session is already connected, or
|
|
82
|
+
// wait for the next `connectionStatusChanged` that flips it to
|
|
83
|
+
// `connected`. Without this the stdin write races the websocket
|
|
84
|
+
// handshake and the agent command silently disappears.
|
|
85
|
+
if (session.connectionStatus === 'connected') {
|
|
86
|
+
sendCommand();
|
|
87
|
+
} else {
|
|
88
|
+
const onStatus = (): void => {
|
|
89
|
+
if (session.connectionStatus === 'connected') {
|
|
90
|
+
session.connectionStatusChanged.disconnect(onStatus);
|
|
91
|
+
sendCommand();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
session.connectionStatusChanged.connect(onStatus);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return main;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
disposables.add(disposable);
|
|
101
|
+
}
|
|
102
|
+
return disposables;
|
|
103
|
+
}
|