@plmbr/notebook-intelligence 5.0.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/LICENSE +674 -0
- package/README.md +412 -0
- package/lib/api.d.ts +288 -0
- package/lib/api.js +927 -0
- package/lib/cell-output-bundle.d.ts +25 -0
- package/lib/cell-output-bundle.js +129 -0
- package/lib/cell-output-toolbar.d.ts +26 -0
- package/lib/cell-output-toolbar.js +188 -0
- package/lib/chat-progress-feedback.d.ts +3 -0
- package/lib/chat-progress-feedback.js +27 -0
- package/lib/chat-sidebar.d.ts +92 -0
- package/lib/chat-sidebar.js +3452 -0
- package/lib/command-ids.d.ts +39 -0
- package/lib/command-ids.js +44 -0
- package/lib/components/ask-user-question.d.ts +2 -0
- package/lib/components/ask-user-question.js +85 -0
- package/lib/components/checkbox.d.ts +2 -0
- package/lib/components/checkbox.js +30 -0
- package/lib/components/claude-mcp-panel.d.ts +2 -0
- package/lib/components/claude-mcp-panel.js +275 -0
- package/lib/components/claude-mcp-paste.d.ts +7 -0
- package/lib/components/claude-mcp-paste.js +104 -0
- package/lib/components/claude-session-picker.d.ts +8 -0
- package/lib/components/claude-session-picker.js +127 -0
- package/lib/components/form-dialog.d.ts +25 -0
- package/lib/components/form-dialog.js +35 -0
- package/lib/components/launcher-picker.d.ts +6 -0
- package/lib/components/launcher-picker.js +135 -0
- package/lib/components/mcp-util.d.ts +2 -0
- package/lib/components/mcp-util.js +37 -0
- package/lib/components/notebook-generation-popover.d.ts +7 -0
- package/lib/components/notebook-generation-popover.js +60 -0
- package/lib/components/pill.d.ts +2 -0
- package/lib/components/pill.js +5 -0
- package/lib/components/plugins-panel.d.ts +3 -0
- package/lib/components/plugins-panel.js +466 -0
- package/lib/components/settings-panel.d.ts +11 -0
- package/lib/components/settings-panel.js +742 -0
- package/lib/components/skills-panel.d.ts +2 -0
- package/lib/components/skills-panel.js +1264 -0
- package/lib/handler.d.ts +8 -0
- package/lib/handler.js +36 -0
- package/lib/icons.d.ts +45 -0
- package/lib/icons.js +54 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +2079 -0
- package/lib/markdown-renderer.d.ts +10 -0
- package/lib/markdown-renderer.js +64 -0
- package/lib/notebook-generation-toolbar.d.ts +16 -0
- package/lib/notebook-generation-toolbar.js +197 -0
- package/lib/notebook-generation.d.ts +8 -0
- package/lib/notebook-generation.js +12 -0
- package/lib/open-file-refresh-watcher-env.d.ts +4 -0
- package/lib/open-file-refresh-watcher-env.js +33 -0
- package/lib/open-file-refresh-watcher.d.ts +97 -0
- package/lib/open-file-refresh-watcher.js +190 -0
- package/lib/shell-utils.d.ts +6 -0
- package/lib/shell-utils.js +9 -0
- package/lib/task-target-notebook.d.ts +2 -0
- package/lib/task-target-notebook.js +28 -0
- package/lib/terminal-drag-format.d.ts +9 -0
- package/lib/terminal-drag-format.js +23 -0
- package/lib/terminal-drag.d.ts +12 -0
- package/lib/terminal-drag.js +268 -0
- package/lib/tokens.d.ts +149 -0
- package/lib/tokens.js +88 -0
- package/lib/tour/tour-anchors.d.ts +18 -0
- package/lib/tour/tour-anchors.js +18 -0
- package/lib/tour/tour-config.d.ts +66 -0
- package/lib/tour/tour-config.js +99 -0
- package/lib/tour/tour-defaults.json +58 -0
- package/lib/tour/tour-events.d.ts +19 -0
- package/lib/tour/tour-events.js +30 -0
- package/lib/tour/tour-overlay.d.ts +6 -0
- package/lib/tour/tour-overlay.js +350 -0
- package/lib/tour/tour-state.d.ts +20 -0
- package/lib/tour/tour-state.js +81 -0
- package/lib/tour/tour-steps.d.ts +33 -0
- package/lib/tour/tour-steps.js +216 -0
- package/lib/utils.d.ts +53 -0
- package/lib/utils.js +385 -0
- package/package.json +258 -0
- package/schema/plugin.json +42 -0
- package/src/api.ts +1424 -0
- package/src/cell-output-bundle.ts +176 -0
- package/src/cell-output-toolbar.ts +232 -0
- package/src/chat-progress-feedback.ts +35 -0
- package/src/chat-sidebar.tsx +5147 -0
- package/src/command-ids.ts +67 -0
- package/src/components/ask-user-question.tsx +151 -0
- package/src/components/checkbox.tsx +62 -0
- package/src/components/claude-mcp-panel.tsx +543 -0
- package/src/components/claude-mcp-paste.ts +132 -0
- package/src/components/claude-session-picker.tsx +214 -0
- package/src/components/form-dialog.tsx +75 -0
- package/src/components/launcher-picker.tsx +237 -0
- package/src/components/mcp-util.ts +53 -0
- package/src/components/notebook-generation-popover.tsx +127 -0
- package/src/components/pill.tsx +15 -0
- package/src/components/plugins-panel.tsx +774 -0
- package/src/components/settings-panel.tsx +1631 -0
- package/src/components/skills-panel.tsx +2084 -0
- package/src/handler.ts +51 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +2583 -0
- package/src/markdown-renderer.tsx +153 -0
- package/src/notebook-generation-toolbar.tsx +281 -0
- package/src/notebook-generation.ts +23 -0
- package/src/open-file-refresh-watcher-env.ts +52 -0
- package/src/open-file-refresh-watcher.ts +260 -0
- package/src/shell-utils.ts +10 -0
- package/src/svg.d.ts +4 -0
- package/src/task-target-notebook.ts +37 -0
- package/src/terminal-drag-format.ts +29 -0
- package/src/terminal-drag.ts +382 -0
- package/src/tokens.ts +171 -0
- package/src/tour/tour-anchors.ts +21 -0
- package/src/tour/tour-config.ts +160 -0
- package/src/tour/tour-events.ts +34 -0
- package/src/tour/tour-overlay.tsx +474 -0
- package/src/tour/tour-state.ts +87 -0
- package/src/tour/tour-steps.ts +281 -0
- package/src/utils.ts +455 -0
- package/style/base.css +3238 -0
- package/style/icons/cell-toolbar-bug.svg +5 -0
- package/style/icons/cell-toolbar-chat.svg +5 -0
- package/style/icons/cell-toolbar-sparkle.svg +5 -0
- package/style/icons/claude.svg +1 -0
- package/style/icons/copilot-warning.svg +1 -0
- package/style/icons/copilot.svg +1 -0
- package/style/icons/copy.svg +1 -0
- package/style/icons/openai.svg +1 -0
- package/style/icons/opencode.svg +1 -0
- package/style/icons/sparkles-warning.svg +5 -0
- package/style/icons/sparkles.svg +1 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { Dialog, showDialog } from '@jupyterlab/apputils';
|
|
5
|
+
import {
|
|
6
|
+
IPluginInfo,
|
|
7
|
+
IPluginMarketplaceInfo,
|
|
8
|
+
IPluginMarketplacePluginInfo,
|
|
9
|
+
NBIAPI,
|
|
10
|
+
PluginScope
|
|
11
|
+
} from '../api';
|
|
12
|
+
import { FormDialog } from './form-dialog';
|
|
13
|
+
|
|
14
|
+
const SCOPES: PluginScope[] = ['user', 'project', 'local'];
|
|
15
|
+
const SCOPE_HINT: Record<PluginScope, string> = {
|
|
16
|
+
user: 'available in all your projects',
|
|
17
|
+
project: 'shared via the project repo',
|
|
18
|
+
local: 'this project, this user only'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function marketplaceName(marketplace: IPluginMarketplaceInfo): string {
|
|
22
|
+
return String(marketplace.name ?? '').trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Plugin-name list trimmed with ellipsis. Caller decides the visible cap;
|
|
26
|
+
// the helper just stitches a comma-separated string and appends the
|
|
27
|
+
// "+N more" tail when the cap is hit, so the row width stays bounded
|
|
28
|
+
// even for marketplaces with hundreds of plugins.
|
|
29
|
+
export function summarizePluginNames(
|
|
30
|
+
names: readonly string[] | undefined,
|
|
31
|
+
visible: number
|
|
32
|
+
): string {
|
|
33
|
+
if (!names || names.length === 0) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
if (names.length <= visible) {
|
|
37
|
+
return names.join(', ');
|
|
38
|
+
}
|
|
39
|
+
const head = names.slice(0, visible).join(', ');
|
|
40
|
+
const remaining = names.length - visible;
|
|
41
|
+
return `${head}, +${remaining} more`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pluginName(plugin: IPluginInfo): string {
|
|
45
|
+
return String(plugin.name ?? plugin.id ?? '').trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pluginEntryLabel(plugin: IPluginMarketplacePluginInfo): string {
|
|
49
|
+
const name = pluginName(plugin);
|
|
50
|
+
const description =
|
|
51
|
+
typeof plugin.description === 'string' ? plugin.description : '';
|
|
52
|
+
return description ? `${name} - ${description}` : name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type PluginInstallMode = 'marketplace' | 'manual';
|
|
56
|
+
|
|
57
|
+
export function SettingsPanelComponentPlugins(_props: any): JSX.Element {
|
|
58
|
+
const [plugins, setPlugins] = useState<IPluginInfo[]>([]);
|
|
59
|
+
const [marketplaces, setMarketplaces] = useState<IPluginMarketplaceInfo[]>(
|
|
60
|
+
[]
|
|
61
|
+
);
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
const [error, setError] = useState<string | null>(null);
|
|
64
|
+
const [installOpen, setInstallOpen] = useState(false);
|
|
65
|
+
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
|
|
66
|
+
// Composite key (`scope:name`) so a user-scope plugin and a project-scope
|
|
67
|
+
// plugin sharing a name don't clobber each other's busy indicators.
|
|
68
|
+
const [busyPluginKey, setBusyPluginKey] = useState<string | null>(null);
|
|
69
|
+
const [busyMarketplace, setBusyMarketplace] = useState<string | null>(null);
|
|
70
|
+
const [allowGithubImport, setAllowGithubImport] = useState(
|
|
71
|
+
NBIAPI.config.allowGithubPluginImport
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const refresh = async () => {
|
|
75
|
+
setLoading(true);
|
|
76
|
+
setError(null);
|
|
77
|
+
try {
|
|
78
|
+
const [p, m] = await Promise.all([
|
|
79
|
+
NBIAPI.listPlugins(),
|
|
80
|
+
NBIAPI.listPluginMarketplaces()
|
|
81
|
+
]);
|
|
82
|
+
setPlugins(p);
|
|
83
|
+
setMarketplaces(m);
|
|
84
|
+
} catch (e: any) {
|
|
85
|
+
setError(e?.message ?? String(e));
|
|
86
|
+
} finally {
|
|
87
|
+
setLoading(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
refresh();
|
|
93
|
+
const handler = () => {
|
|
94
|
+
setAllowGithubImport(NBIAPI.config.allowGithubPluginImport);
|
|
95
|
+
};
|
|
96
|
+
NBIAPI.configChanged.connect(handler);
|
|
97
|
+
return () => {
|
|
98
|
+
NBIAPI.configChanged.disconnect(handler);
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
const handleUninstall = async (p: IPluginInfo) => {
|
|
103
|
+
const name = pluginName(p);
|
|
104
|
+
const scope = (p.scope as PluginScope) ?? 'user';
|
|
105
|
+
if (!name) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const ok = await showDialog({
|
|
109
|
+
title: 'Uninstall plugin?',
|
|
110
|
+
body: `"${name}" will be removed from Claude's ${scope}-scope config.`,
|
|
111
|
+
buttons: [
|
|
112
|
+
Dialog.cancelButton(),
|
|
113
|
+
Dialog.warnButton({ label: 'Uninstall' })
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
if (!ok.button.accept) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const busyKey = `${scope}:${name}`;
|
|
120
|
+
setBusyPluginKey(busyKey);
|
|
121
|
+
try {
|
|
122
|
+
await NBIAPI.uninstallPlugin(name, scope);
|
|
123
|
+
await refresh();
|
|
124
|
+
} catch (e: any) {
|
|
125
|
+
setError(`Failed to uninstall: ${e?.message ?? e}`);
|
|
126
|
+
} finally {
|
|
127
|
+
setBusyPluginKey(null);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleToggleEnabled = async (p: IPluginInfo) => {
|
|
132
|
+
const name = pluginName(p);
|
|
133
|
+
const scope = (p.scope as PluginScope) ?? 'user';
|
|
134
|
+
if (!name) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const busyKey = `${scope}:${name}`;
|
|
138
|
+
setBusyPluginKey(busyKey);
|
|
139
|
+
try {
|
|
140
|
+
await NBIAPI.setPluginEnabled(name, scope, !p.enabled);
|
|
141
|
+
await refresh();
|
|
142
|
+
} catch (e: any) {
|
|
143
|
+
setError(`Failed to update: ${e?.message ?? e}`);
|
|
144
|
+
} finally {
|
|
145
|
+
setBusyPluginKey(null);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleRemoveMarketplace = async (m: IPluginMarketplaceInfo) => {
|
|
150
|
+
const name = String(m.name ?? '');
|
|
151
|
+
if (!name) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const ok = await showDialog({
|
|
155
|
+
title: 'Remove marketplace?',
|
|
156
|
+
body: `"${name}" will be removed from Claude's plugin marketplaces.`,
|
|
157
|
+
buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })]
|
|
158
|
+
});
|
|
159
|
+
if (!ok.button.accept) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setBusyMarketplace(name);
|
|
163
|
+
try {
|
|
164
|
+
await NBIAPI.removePluginMarketplace(name);
|
|
165
|
+
await refresh();
|
|
166
|
+
} catch (e: any) {
|
|
167
|
+
setError(`Failed to remove: ${e?.message ?? e}`);
|
|
168
|
+
} finally {
|
|
169
|
+
setBusyMarketplace(null);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleUpdateMarketplace = async (m: IPluginMarketplaceInfo) => {
|
|
174
|
+
const name = String(m.name ?? '');
|
|
175
|
+
if (!name) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
setBusyMarketplace(name);
|
|
179
|
+
try {
|
|
180
|
+
await NBIAPI.updatePluginMarketplace(name);
|
|
181
|
+
await refresh();
|
|
182
|
+
} catch (e: any) {
|
|
183
|
+
setError(`Failed to update marketplace: ${e?.message ?? e}`);
|
|
184
|
+
} finally {
|
|
185
|
+
setBusyMarketplace(null);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const grouped: Record<PluginScope, IPluginInfo[]> = {
|
|
190
|
+
user: [],
|
|
191
|
+
project: [],
|
|
192
|
+
local: []
|
|
193
|
+
};
|
|
194
|
+
for (const p of plugins) {
|
|
195
|
+
const scope = (p.scope as PluginScope) ?? 'user';
|
|
196
|
+
(grouped[scope] ?? grouped.user).push(p);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="config-dialog-body nbi-skills-panel">
|
|
201
|
+
<div className="nbi-skills-header">
|
|
202
|
+
<div className="nbi-skills-title">Plugins</div>
|
|
203
|
+
<div className="nbi-skills-header-actions">
|
|
204
|
+
<button
|
|
205
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
206
|
+
onClick={refresh}
|
|
207
|
+
disabled={loading}
|
|
208
|
+
>
|
|
209
|
+
<div className="jp-Dialog-buttonLabel">
|
|
210
|
+
{loading ? 'Refreshing…' : 'Refresh'}
|
|
211
|
+
</div>
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
215
|
+
onClick={() => setMarketplaceOpen(true)}
|
|
216
|
+
>
|
|
217
|
+
<div className="jp-Dialog-buttonLabel">Add marketplace</div>
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
|
|
221
|
+
onClick={() => setInstallOpen(true)}
|
|
222
|
+
>
|
|
223
|
+
<div className="jp-Dialog-buttonLabel">Install plugin</div>
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="nbi-info-banner" role="note">
|
|
229
|
+
Add a marketplace to discover plugins, then install one to extend Claude
|
|
230
|
+
Code with new commands, agents, and tool integrations.
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{error && (
|
|
234
|
+
<div className="nbi-skills-error" role="alert">
|
|
235
|
+
{error}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<div className="nbi-skills-section">
|
|
240
|
+
<div className="nbi-skills-section-caption">MARKETPLACES</div>
|
|
241
|
+
{marketplaces.length === 0 ? (
|
|
242
|
+
<div className="nbi-skills-empty">
|
|
243
|
+
{loading
|
|
244
|
+
? 'Loading…'
|
|
245
|
+
: 'No marketplaces configured. Add one to discover plugins.'}
|
|
246
|
+
</div>
|
|
247
|
+
) : (
|
|
248
|
+
marketplaces.map((m, i) => (
|
|
249
|
+
<MarketplaceRow
|
|
250
|
+
key={String(m.name ?? m.source ?? i)}
|
|
251
|
+
info={m}
|
|
252
|
+
busy={busyMarketplace === String(m.name ?? '')}
|
|
253
|
+
onRemove={() => handleRemoveMarketplace(m)}
|
|
254
|
+
onUpdate={() => handleUpdateMarketplace(m)}
|
|
255
|
+
/>
|
|
256
|
+
))
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{SCOPES.map(scope => (
|
|
261
|
+
<PluginScopeSection
|
|
262
|
+
key={scope}
|
|
263
|
+
scope={scope}
|
|
264
|
+
plugins={grouped[scope]}
|
|
265
|
+
loading={loading}
|
|
266
|
+
busyPluginKey={busyPluginKey}
|
|
267
|
+
onUninstall={handleUninstall}
|
|
268
|
+
onToggle={handleToggleEnabled}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
271
|
+
|
|
272
|
+
{installOpen && (
|
|
273
|
+
<PluginInstallDialog
|
|
274
|
+
marketplaces={marketplaces}
|
|
275
|
+
onCancel={() => setInstallOpen(false)}
|
|
276
|
+
onSubmit={async ({ plugin, scope }) => {
|
|
277
|
+
await NBIAPI.installPlugin(plugin, scope);
|
|
278
|
+
setInstallOpen(false);
|
|
279
|
+
await refresh();
|
|
280
|
+
}}
|
|
281
|
+
/>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{marketplaceOpen && (
|
|
285
|
+
<MarketplaceAddDialog
|
|
286
|
+
allowGithubImport={allowGithubImport}
|
|
287
|
+
onCancel={() => setMarketplaceOpen(false)}
|
|
288
|
+
onSubmit={async ({ source, scope }) => {
|
|
289
|
+
await NBIAPI.addPluginMarketplace(source, scope);
|
|
290
|
+
setMarketplaceOpen(false);
|
|
291
|
+
await refresh();
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function PluginScopeSection(props: {
|
|
300
|
+
scope: PluginScope;
|
|
301
|
+
plugins: IPluginInfo[];
|
|
302
|
+
loading: boolean;
|
|
303
|
+
busyPluginKey: string | null;
|
|
304
|
+
onUninstall: (p: IPluginInfo) => void;
|
|
305
|
+
onToggle: (p: IPluginInfo) => void;
|
|
306
|
+
}) {
|
|
307
|
+
return (
|
|
308
|
+
<div className="nbi-skills-section">
|
|
309
|
+
<div
|
|
310
|
+
className="nbi-skills-section-caption"
|
|
311
|
+
title={SCOPE_HINT[props.scope]}
|
|
312
|
+
>
|
|
313
|
+
{props.scope.toUpperCase()}
|
|
314
|
+
</div>
|
|
315
|
+
{props.plugins.length === 0 ? (
|
|
316
|
+
<div className="nbi-skills-empty">
|
|
317
|
+
{props.loading ? 'Loading…' : 'No plugins in this scope.'}
|
|
318
|
+
</div>
|
|
319
|
+
) : (
|
|
320
|
+
props.plugins.map(p => {
|
|
321
|
+
const scope = (p.scope as PluginScope) ?? props.scope;
|
|
322
|
+
const name = pluginName(p);
|
|
323
|
+
const rowKey = `${scope}:${name}`;
|
|
324
|
+
return (
|
|
325
|
+
<PluginRow
|
|
326
|
+
key={rowKey}
|
|
327
|
+
plugin={p}
|
|
328
|
+
busy={props.busyPluginKey === rowKey}
|
|
329
|
+
onUninstall={() => props.onUninstall(p)}
|
|
330
|
+
onToggle={() => props.onToggle(p)}
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
})
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function PluginRow(props: {
|
|
340
|
+
plugin: IPluginInfo;
|
|
341
|
+
busy: boolean;
|
|
342
|
+
onUninstall: () => void;
|
|
343
|
+
onToggle: () => void;
|
|
344
|
+
}) {
|
|
345
|
+
const { plugin } = props;
|
|
346
|
+
const description = [plugin.version, plugin.marketplace, plugin.description]
|
|
347
|
+
.filter(Boolean)
|
|
348
|
+
.map(String)
|
|
349
|
+
.join(' · ');
|
|
350
|
+
const enabled = plugin.enabled !== false;
|
|
351
|
+
return (
|
|
352
|
+
<div className="nbi-skill-row">
|
|
353
|
+
<div className="nbi-skill-row-main">
|
|
354
|
+
<div className="nbi-skill-row-name">
|
|
355
|
+
{pluginName(plugin) || '(unnamed)'}
|
|
356
|
+
{!enabled && <span> — disabled</span>}
|
|
357
|
+
</div>
|
|
358
|
+
{description && (
|
|
359
|
+
<div className="nbi-skill-row-description">{description}</div>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
<div className="nbi-skill-row-actions" onClick={e => e.stopPropagation()}>
|
|
363
|
+
<button
|
|
364
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
365
|
+
onClick={props.onToggle}
|
|
366
|
+
disabled={props.busy}
|
|
367
|
+
>
|
|
368
|
+
<div className="jp-Dialog-buttonLabel">
|
|
369
|
+
{enabled ? 'Disable' : 'Enable'}
|
|
370
|
+
</div>
|
|
371
|
+
</button>
|
|
372
|
+
<button
|
|
373
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
374
|
+
onClick={props.onUninstall}
|
|
375
|
+
disabled={props.busy}
|
|
376
|
+
>
|
|
377
|
+
<div className="jp-Dialog-buttonLabel">
|
|
378
|
+
{props.busy ? 'Working…' : 'Uninstall'}
|
|
379
|
+
</div>
|
|
380
|
+
</button>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Visible plugin-name cap on the marketplace row. Picked so a typical
|
|
387
|
+
// marketplace (a handful of plugins) fits on one line, with longer lists
|
|
388
|
+
// collapsing to "a, b, c, +N more" so the row width stays bounded.
|
|
389
|
+
const MARKETPLACE_VISIBLE_PLUGINS = 5;
|
|
390
|
+
|
|
391
|
+
function MarketplaceRow(props: {
|
|
392
|
+
info: IPluginMarketplaceInfo;
|
|
393
|
+
busy: boolean;
|
|
394
|
+
onRemove: () => void;
|
|
395
|
+
onUpdate: () => void;
|
|
396
|
+
}) {
|
|
397
|
+
const { info } = props;
|
|
398
|
+
const description =
|
|
399
|
+
typeof info.description === 'string' ? info.description.trim() : '';
|
|
400
|
+
const version = typeof info.version === 'string' ? info.version.trim() : '';
|
|
401
|
+
const pluginNames = Array.isArray(info.plugin_names)
|
|
402
|
+
? info.plugin_names.filter((n): n is string => typeof n === 'string')
|
|
403
|
+
: [];
|
|
404
|
+
const pluginCount =
|
|
405
|
+
typeof info.plugin_count === 'number'
|
|
406
|
+
? info.plugin_count
|
|
407
|
+
: pluginNames.length;
|
|
408
|
+
const pluginSummary = summarizePluginNames(
|
|
409
|
+
pluginNames,
|
|
410
|
+
MARKETPLACE_VISIBLE_PLUGINS
|
|
411
|
+
);
|
|
412
|
+
// Pluralize for 0/1/N: "no plugins" reads better than "0 plugins" when
|
|
413
|
+
// the marketplace manifest has not yet been refreshed and we have no
|
|
414
|
+
// plugin entries.
|
|
415
|
+
const pluginCountLabel =
|
|
416
|
+
pluginCount === 0
|
|
417
|
+
? 'no plugins'
|
|
418
|
+
: pluginCount === 1
|
|
419
|
+
? '1 plugin'
|
|
420
|
+
: `${pluginCount} plugins`;
|
|
421
|
+
// Only render the plugin summary line when the manifest read produced
|
|
422
|
+
// a count or name list. Without this guard, a marketplace that the
|
|
423
|
+
// user just added but whose manifest hasn't been cached yet would
|
|
424
|
+
// render "no plugins" and look broken; here it just renders no line
|
|
425
|
+
// at all until the next refresh picks up the manifest.
|
|
426
|
+
const hasPluginManifestData =
|
|
427
|
+
Array.isArray(info.plugin_names) || typeof info.plugin_count === 'number';
|
|
428
|
+
return (
|
|
429
|
+
<div className="nbi-skill-row">
|
|
430
|
+
<div className="nbi-skill-row-main">
|
|
431
|
+
<div className="nbi-skill-row-name">
|
|
432
|
+
{String(info.name ?? '(unnamed)')}
|
|
433
|
+
{version && (
|
|
434
|
+
<span className="nbi-skill-row-version"> v{version}</span>
|
|
435
|
+
)}
|
|
436
|
+
</div>
|
|
437
|
+
{description && (
|
|
438
|
+
<div className="nbi-skill-row-description">{description}</div>
|
|
439
|
+
)}
|
|
440
|
+
{info.source && (
|
|
441
|
+
<div className="nbi-skill-row-description">
|
|
442
|
+
<code>{String(info.source)}</code>
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
{hasPluginManifestData && (
|
|
446
|
+
<div className="nbi-skill-row-description nbi-skill-row-plugins">
|
|
447
|
+
{pluginCountLabel}
|
|
448
|
+
{pluginSummary && <span>{`: ${pluginSummary}`}</span>}
|
|
449
|
+
</div>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
<div className="nbi-skill-row-actions" onClick={e => e.stopPropagation()}>
|
|
453
|
+
<button
|
|
454
|
+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
|
|
455
|
+
onClick={props.onUpdate}
|
|
456
|
+
disabled={props.busy}
|
|
457
|
+
title="Refresh this marketplace from its source"
|
|
458
|
+
>
|
|
459
|
+
<div className="jp-Dialog-buttonLabel">
|
|
460
|
+
{props.busy ? 'Updating…' : 'Update'}
|
|
461
|
+
</div>
|
|
462
|
+
</button>
|
|
463
|
+
<button
|
|
464
|
+
className="jp-Dialog-button jp-mod-reject jp-mod-styled"
|
|
465
|
+
onClick={props.onRemove}
|
|
466
|
+
disabled={props.busy}
|
|
467
|
+
>
|
|
468
|
+
<div className="jp-Dialog-buttonLabel">
|
|
469
|
+
{props.busy ? 'Removing…' : 'Remove'}
|
|
470
|
+
</div>
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function PluginInstallDialog(props: {
|
|
478
|
+
marketplaces: IPluginMarketplaceInfo[];
|
|
479
|
+
onCancel: () => void;
|
|
480
|
+
onSubmit: (input: { plugin: string; scope: PluginScope }) => Promise<void>;
|
|
481
|
+
}) {
|
|
482
|
+
const marketplaceNames = props.marketplaces
|
|
483
|
+
.map(marketplaceName)
|
|
484
|
+
.filter(Boolean);
|
|
485
|
+
const marketplaceKey = marketplaceNames.join('\n');
|
|
486
|
+
const [installMode, setInstallMode] = useState<PluginInstallMode>(
|
|
487
|
+
marketplaceNames.length > 0 ? 'marketplace' : 'manual'
|
|
488
|
+
);
|
|
489
|
+
const [marketplace, setMarketplace] = useState(marketplaceNames[0] ?? '');
|
|
490
|
+
const [marketplacePlugins, setMarketplacePlugins] = useState<
|
|
491
|
+
IPluginMarketplacePluginInfo[]
|
|
492
|
+
>([]);
|
|
493
|
+
const [selectedPlugin, setSelectedPlugin] = useState('');
|
|
494
|
+
const [manualPlugin, setManualPlugin] = useState('');
|
|
495
|
+
const [scope, setScope] = useState<PluginScope>('user');
|
|
496
|
+
const [loadingPlugins, setLoadingPlugins] = useState(false);
|
|
497
|
+
const [pluginListError, setPluginListError] = useState<string | null>(null);
|
|
498
|
+
const [submitting, setSubmitting] = useState(false);
|
|
499
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
500
|
+
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (marketplace && marketplaceNames.includes(marketplace)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
setMarketplace(marketplaceNames[0] ?? '');
|
|
506
|
+
}, [marketplace, marketplaceKey]);
|
|
507
|
+
|
|
508
|
+
useEffect(() => {
|
|
509
|
+
let cancelled = false;
|
|
510
|
+
if (installMode !== 'marketplace') {
|
|
511
|
+
setLoadingPlugins(false);
|
|
512
|
+
setPluginListError(null);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (!marketplace) {
|
|
516
|
+
setMarketplacePlugins([]);
|
|
517
|
+
setSelectedPlugin('');
|
|
518
|
+
setPluginListError(null);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
setLoadingPlugins(true);
|
|
523
|
+
setPluginListError(null);
|
|
524
|
+
NBIAPI.listPluginMarketplacePlugins(marketplace)
|
|
525
|
+
.then(plugins => {
|
|
526
|
+
if (cancelled) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const namedPlugins = plugins.filter(plugin => pluginName(plugin));
|
|
530
|
+
setMarketplacePlugins(namedPlugins);
|
|
531
|
+
const firstPluginName =
|
|
532
|
+
namedPlugins.length > 0 ? pluginName(namedPlugins[0]) : '';
|
|
533
|
+
setSelectedPlugin(current =>
|
|
534
|
+
namedPlugins.some(plugin => pluginName(plugin) === current)
|
|
535
|
+
? current
|
|
536
|
+
: firstPluginName
|
|
537
|
+
);
|
|
538
|
+
})
|
|
539
|
+
.catch((e: any) => {
|
|
540
|
+
if (cancelled) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
setMarketplacePlugins([]);
|
|
544
|
+
setSelectedPlugin('');
|
|
545
|
+
setPluginListError(`Failed to load plugins: ${e?.message ?? e}`);
|
|
546
|
+
})
|
|
547
|
+
.finally(() => {
|
|
548
|
+
if (!cancelled) {
|
|
549
|
+
setLoadingPlugins(false);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
return () => {
|
|
554
|
+
cancelled = true;
|
|
555
|
+
};
|
|
556
|
+
}, [installMode, marketplace]);
|
|
557
|
+
|
|
558
|
+
const trimmedManualPlugin = manualPlugin.trim();
|
|
559
|
+
const canSubmit =
|
|
560
|
+
!submitting &&
|
|
561
|
+
(installMode === 'manual'
|
|
562
|
+
? Boolean(trimmedManualPlugin)
|
|
563
|
+
: Boolean(marketplace && selectedPlugin && !loadingPlugins));
|
|
564
|
+
|
|
565
|
+
const handleSubmit = async () => {
|
|
566
|
+
if (!canSubmit) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
setSubmitError(null);
|
|
570
|
+
setSubmitting(true);
|
|
571
|
+
try {
|
|
572
|
+
await props.onSubmit({
|
|
573
|
+
plugin:
|
|
574
|
+
installMode === 'manual'
|
|
575
|
+
? trimmedManualPlugin
|
|
576
|
+
: `${selectedPlugin}@${marketplace}`,
|
|
577
|
+
scope
|
|
578
|
+
});
|
|
579
|
+
} catch (e: any) {
|
|
580
|
+
setSubmitError(e?.message ?? String(e));
|
|
581
|
+
} finally {
|
|
582
|
+
setSubmitting(false);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
<FormDialog
|
|
588
|
+
title="Install plugin"
|
|
589
|
+
submitLabel="Install"
|
|
590
|
+
submitInProgressLabel="Installing…"
|
|
591
|
+
canSubmit={Boolean(canSubmit)}
|
|
592
|
+
submitting={submitting}
|
|
593
|
+
error={submitError}
|
|
594
|
+
onCancel={props.onCancel}
|
|
595
|
+
onSubmit={handleSubmit}
|
|
596
|
+
>
|
|
597
|
+
<div className="nbi-form-field">
|
|
598
|
+
<label>Install from</label>
|
|
599
|
+
<select
|
|
600
|
+
value={installMode}
|
|
601
|
+
onChange={e => setInstallMode(e.target.value as PluginInstallMode)}
|
|
602
|
+
disabled={submitting}
|
|
603
|
+
autoFocus
|
|
604
|
+
>
|
|
605
|
+
<option value="marketplace" disabled={marketplaceNames.length === 0}>
|
|
606
|
+
Marketplace picker
|
|
607
|
+
</option>
|
|
608
|
+
<option value="manual">Specify manually</option>
|
|
609
|
+
</select>
|
|
610
|
+
</div>
|
|
611
|
+
{installMode === 'manual' ? (
|
|
612
|
+
<>
|
|
613
|
+
<div className="nbi-form-field">
|
|
614
|
+
<label>Plugin</label>
|
|
615
|
+
<input
|
|
616
|
+
type="text"
|
|
617
|
+
value={manualPlugin}
|
|
618
|
+
onChange={e => setManualPlugin(e.target.value)}
|
|
619
|
+
placeholder="plugin@marketplace"
|
|
620
|
+
disabled={submitting}
|
|
621
|
+
/>
|
|
622
|
+
</div>
|
|
623
|
+
<div className="nbi-form-hint">
|
|
624
|
+
Enter a plugin name or the plugin@marketplace shorthand.
|
|
625
|
+
</div>
|
|
626
|
+
</>
|
|
627
|
+
) : (
|
|
628
|
+
<>
|
|
629
|
+
<div className="nbi-form-field">
|
|
630
|
+
<label>Marketplace</label>
|
|
631
|
+
<select
|
|
632
|
+
value={marketplace}
|
|
633
|
+
onChange={e => setMarketplace(e.target.value)}
|
|
634
|
+
disabled={submitting}
|
|
635
|
+
>
|
|
636
|
+
{marketplaceNames.map(name => (
|
|
637
|
+
<option key={name} value={name}>
|
|
638
|
+
{name}
|
|
639
|
+
</option>
|
|
640
|
+
))}
|
|
641
|
+
</select>
|
|
642
|
+
</div>
|
|
643
|
+
<div className="nbi-form-field">
|
|
644
|
+
<label>Plugin</label>
|
|
645
|
+
<select
|
|
646
|
+
value={selectedPlugin}
|
|
647
|
+
onChange={e => setSelectedPlugin(e.target.value)}
|
|
648
|
+
disabled={
|
|
649
|
+
submitting || loadingPlugins || marketplacePlugins.length === 0
|
|
650
|
+
}
|
|
651
|
+
>
|
|
652
|
+
{marketplacePlugins.map(plugin => {
|
|
653
|
+
const name = pluginName(plugin);
|
|
654
|
+
return (
|
|
655
|
+
<option key={name} value={name}>
|
|
656
|
+
{pluginEntryLabel(plugin)}
|
|
657
|
+
</option>
|
|
658
|
+
);
|
|
659
|
+
})}
|
|
660
|
+
</select>
|
|
661
|
+
</div>
|
|
662
|
+
{loadingPlugins && (
|
|
663
|
+
<div className="nbi-form-hint">Loading marketplace plugins...</div>
|
|
664
|
+
)}
|
|
665
|
+
{!loadingPlugins &&
|
|
666
|
+
!pluginListError &&
|
|
667
|
+
marketplacePlugins.length === 0 && (
|
|
668
|
+
<div className="nbi-form-hint">
|
|
669
|
+
No plugins were found in this marketplace.
|
|
670
|
+
</div>
|
|
671
|
+
)}
|
|
672
|
+
{pluginListError && (
|
|
673
|
+
<div className="nbi-skills-error" role="alert">
|
|
674
|
+
{pluginListError}
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
677
|
+
</>
|
|
678
|
+
)}
|
|
679
|
+
<div className="nbi-form-field">
|
|
680
|
+
<label>Scope</label>
|
|
681
|
+
<select
|
|
682
|
+
value={scope}
|
|
683
|
+
onChange={e => setScope(e.target.value as PluginScope)}
|
|
684
|
+
>
|
|
685
|
+
{SCOPES.map(s => (
|
|
686
|
+
<option key={s} value={s}>
|
|
687
|
+
{s} — {SCOPE_HINT[s]}
|
|
688
|
+
</option>
|
|
689
|
+
))}
|
|
690
|
+
</select>
|
|
691
|
+
</div>
|
|
692
|
+
</FormDialog>
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function MarketplaceAddDialog(props: {
|
|
697
|
+
allowGithubImport: boolean;
|
|
698
|
+
onCancel: () => void;
|
|
699
|
+
onSubmit: (input: { source: string; scope: PluginScope }) => Promise<void>;
|
|
700
|
+
}) {
|
|
701
|
+
const [source, setSource] = useState('');
|
|
702
|
+
const [scope, setScope] = useState<PluginScope>('user');
|
|
703
|
+
const [submitting, setSubmitting] = useState(false);
|
|
704
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
705
|
+
|
|
706
|
+
const canSubmit = source.trim() && !submitting;
|
|
707
|
+
|
|
708
|
+
const handleSubmit = async () => {
|
|
709
|
+
if (!canSubmit) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
setSubmitError(null);
|
|
713
|
+
setSubmitting(true);
|
|
714
|
+
try {
|
|
715
|
+
await props.onSubmit({ source: source.trim(), scope });
|
|
716
|
+
} catch (e: any) {
|
|
717
|
+
setSubmitError(e?.message ?? String(e));
|
|
718
|
+
} finally {
|
|
719
|
+
setSubmitting(false);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<FormDialog
|
|
725
|
+
title="Add plugin marketplace"
|
|
726
|
+
submitLabel="Add"
|
|
727
|
+
submitInProgressLabel="Adding…"
|
|
728
|
+
canSubmit={Boolean(canSubmit)}
|
|
729
|
+
submitting={submitting}
|
|
730
|
+
error={submitError}
|
|
731
|
+
onCancel={props.onCancel}
|
|
732
|
+
onSubmit={handleSubmit}
|
|
733
|
+
>
|
|
734
|
+
<div className="nbi-form-field">
|
|
735
|
+
<label>Source</label>
|
|
736
|
+
<input
|
|
737
|
+
type="text"
|
|
738
|
+
value={source}
|
|
739
|
+
onChange={e => setSource(e.target.value)}
|
|
740
|
+
placeholder={
|
|
741
|
+
props.allowGithubImport
|
|
742
|
+
? 'owner/repo, https://github.com/owner/repo, or local path'
|
|
743
|
+
: 'https://… or local path'
|
|
744
|
+
}
|
|
745
|
+
autoFocus
|
|
746
|
+
/>
|
|
747
|
+
</div>
|
|
748
|
+
{props.allowGithubImport ? (
|
|
749
|
+
<div className="nbi-form-hint">
|
|
750
|
+
Private GitHub sources work if <code>GITHUB_TOKEN</code> is set or{' '}
|
|
751
|
+
<code>gh auth login</code> is configured on the Jupyter server.
|
|
752
|
+
</div>
|
|
753
|
+
) : (
|
|
754
|
+
<div className="nbi-form-hint">
|
|
755
|
+
GitHub-sourced marketplaces are disabled by your administrator. Use a
|
|
756
|
+
non-GitHub URL or a local filesystem path.
|
|
757
|
+
</div>
|
|
758
|
+
)}
|
|
759
|
+
<div className="nbi-form-field">
|
|
760
|
+
<label>Scope</label>
|
|
761
|
+
<select
|
|
762
|
+
value={scope}
|
|
763
|
+
onChange={e => setScope(e.target.value as PluginScope)}
|
|
764
|
+
>
|
|
765
|
+
{SCOPES.map(s => (
|
|
766
|
+
<option key={s} value={s}>
|
|
767
|
+
{s} — {SCOPE_HINT[s]}
|
|
768
|
+
</option>
|
|
769
|
+
))}
|
|
770
|
+
</select>
|
|
771
|
+
</div>
|
|
772
|
+
</FormDialog>
|
|
773
|
+
);
|
|
774
|
+
}
|