@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.
Files changed (137) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +412 -0
  3. package/lib/api.d.ts +288 -0
  4. package/lib/api.js +927 -0
  5. package/lib/cell-output-bundle.d.ts +25 -0
  6. package/lib/cell-output-bundle.js +129 -0
  7. package/lib/cell-output-toolbar.d.ts +26 -0
  8. package/lib/cell-output-toolbar.js +188 -0
  9. package/lib/chat-progress-feedback.d.ts +3 -0
  10. package/lib/chat-progress-feedback.js +27 -0
  11. package/lib/chat-sidebar.d.ts +92 -0
  12. package/lib/chat-sidebar.js +3452 -0
  13. package/lib/command-ids.d.ts +39 -0
  14. package/lib/command-ids.js +44 -0
  15. package/lib/components/ask-user-question.d.ts +2 -0
  16. package/lib/components/ask-user-question.js +85 -0
  17. package/lib/components/checkbox.d.ts +2 -0
  18. package/lib/components/checkbox.js +30 -0
  19. package/lib/components/claude-mcp-panel.d.ts +2 -0
  20. package/lib/components/claude-mcp-panel.js +275 -0
  21. package/lib/components/claude-mcp-paste.d.ts +7 -0
  22. package/lib/components/claude-mcp-paste.js +104 -0
  23. package/lib/components/claude-session-picker.d.ts +8 -0
  24. package/lib/components/claude-session-picker.js +127 -0
  25. package/lib/components/form-dialog.d.ts +25 -0
  26. package/lib/components/form-dialog.js +35 -0
  27. package/lib/components/launcher-picker.d.ts +6 -0
  28. package/lib/components/launcher-picker.js +135 -0
  29. package/lib/components/mcp-util.d.ts +2 -0
  30. package/lib/components/mcp-util.js +37 -0
  31. package/lib/components/notebook-generation-popover.d.ts +7 -0
  32. package/lib/components/notebook-generation-popover.js +60 -0
  33. package/lib/components/pill.d.ts +2 -0
  34. package/lib/components/pill.js +5 -0
  35. package/lib/components/plugins-panel.d.ts +3 -0
  36. package/lib/components/plugins-panel.js +466 -0
  37. package/lib/components/settings-panel.d.ts +11 -0
  38. package/lib/components/settings-panel.js +742 -0
  39. package/lib/components/skills-panel.d.ts +2 -0
  40. package/lib/components/skills-panel.js +1264 -0
  41. package/lib/handler.d.ts +8 -0
  42. package/lib/handler.js +36 -0
  43. package/lib/icons.d.ts +45 -0
  44. package/lib/icons.js +54 -0
  45. package/lib/index.d.ts +8 -0
  46. package/lib/index.js +2079 -0
  47. package/lib/markdown-renderer.d.ts +10 -0
  48. package/lib/markdown-renderer.js +64 -0
  49. package/lib/notebook-generation-toolbar.d.ts +16 -0
  50. package/lib/notebook-generation-toolbar.js +197 -0
  51. package/lib/notebook-generation.d.ts +8 -0
  52. package/lib/notebook-generation.js +12 -0
  53. package/lib/open-file-refresh-watcher-env.d.ts +4 -0
  54. package/lib/open-file-refresh-watcher-env.js +33 -0
  55. package/lib/open-file-refresh-watcher.d.ts +97 -0
  56. package/lib/open-file-refresh-watcher.js +190 -0
  57. package/lib/shell-utils.d.ts +6 -0
  58. package/lib/shell-utils.js +9 -0
  59. package/lib/task-target-notebook.d.ts +2 -0
  60. package/lib/task-target-notebook.js +28 -0
  61. package/lib/terminal-drag-format.d.ts +9 -0
  62. package/lib/terminal-drag-format.js +23 -0
  63. package/lib/terminal-drag.d.ts +12 -0
  64. package/lib/terminal-drag.js +268 -0
  65. package/lib/tokens.d.ts +149 -0
  66. package/lib/tokens.js +88 -0
  67. package/lib/tour/tour-anchors.d.ts +18 -0
  68. package/lib/tour/tour-anchors.js +18 -0
  69. package/lib/tour/tour-config.d.ts +66 -0
  70. package/lib/tour/tour-config.js +99 -0
  71. package/lib/tour/tour-defaults.json +58 -0
  72. package/lib/tour/tour-events.d.ts +19 -0
  73. package/lib/tour/tour-events.js +30 -0
  74. package/lib/tour/tour-overlay.d.ts +6 -0
  75. package/lib/tour/tour-overlay.js +350 -0
  76. package/lib/tour/tour-state.d.ts +20 -0
  77. package/lib/tour/tour-state.js +81 -0
  78. package/lib/tour/tour-steps.d.ts +33 -0
  79. package/lib/tour/tour-steps.js +216 -0
  80. package/lib/utils.d.ts +53 -0
  81. package/lib/utils.js +385 -0
  82. package/package.json +258 -0
  83. package/schema/plugin.json +42 -0
  84. package/src/api.ts +1424 -0
  85. package/src/cell-output-bundle.ts +176 -0
  86. package/src/cell-output-toolbar.ts +232 -0
  87. package/src/chat-progress-feedback.ts +35 -0
  88. package/src/chat-sidebar.tsx +5147 -0
  89. package/src/command-ids.ts +67 -0
  90. package/src/components/ask-user-question.tsx +151 -0
  91. package/src/components/checkbox.tsx +62 -0
  92. package/src/components/claude-mcp-panel.tsx +543 -0
  93. package/src/components/claude-mcp-paste.ts +132 -0
  94. package/src/components/claude-session-picker.tsx +214 -0
  95. package/src/components/form-dialog.tsx +75 -0
  96. package/src/components/launcher-picker.tsx +237 -0
  97. package/src/components/mcp-util.ts +53 -0
  98. package/src/components/notebook-generation-popover.tsx +127 -0
  99. package/src/components/pill.tsx +15 -0
  100. package/src/components/plugins-panel.tsx +774 -0
  101. package/src/components/settings-panel.tsx +1631 -0
  102. package/src/components/skills-panel.tsx +2084 -0
  103. package/src/handler.ts +51 -0
  104. package/src/icons.ts +71 -0
  105. package/src/index.ts +2583 -0
  106. package/src/markdown-renderer.tsx +153 -0
  107. package/src/notebook-generation-toolbar.tsx +281 -0
  108. package/src/notebook-generation.ts +23 -0
  109. package/src/open-file-refresh-watcher-env.ts +52 -0
  110. package/src/open-file-refresh-watcher.ts +260 -0
  111. package/src/shell-utils.ts +10 -0
  112. package/src/svg.d.ts +4 -0
  113. package/src/task-target-notebook.ts +37 -0
  114. package/src/terminal-drag-format.ts +29 -0
  115. package/src/terminal-drag.ts +382 -0
  116. package/src/tokens.ts +171 -0
  117. package/src/tour/tour-anchors.ts +21 -0
  118. package/src/tour/tour-config.ts +160 -0
  119. package/src/tour/tour-events.ts +34 -0
  120. package/src/tour/tour-overlay.tsx +474 -0
  121. package/src/tour/tour-state.ts +87 -0
  122. package/src/tour/tour-steps.ts +281 -0
  123. package/src/utils.ts +455 -0
  124. package/style/base.css +3238 -0
  125. package/style/icons/cell-toolbar-bug.svg +5 -0
  126. package/style/icons/cell-toolbar-chat.svg +5 -0
  127. package/style/icons/cell-toolbar-sparkle.svg +5 -0
  128. package/style/icons/claude.svg +1 -0
  129. package/style/icons/copilot-warning.svg +1 -0
  130. package/style/icons/copilot.svg +1 -0
  131. package/style/icons/copy.svg +1 -0
  132. package/style/icons/openai.svg +1 -0
  133. package/style/icons/opencode.svg +1 -0
  134. package/style/icons/sparkles-warning.svg +5 -0
  135. package/style/icons/sparkles.svg +1 -0
  136. package/style/index.css +1 -0
  137. package/style/index.js +1 -0
@@ -0,0 +1,1264 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
4
+ import { VscCopy, VscEdit, VscTrash } from '../icons';
5
+ import { NBIAPI } from '../api';
6
+ // Closes the enclosing modal on document-level Escape, regardless of which
7
+ // element inside the dialog has focus. The previous per-input handler only
8
+ // fired while the URL field was focused, leaving keyboard users stuck once
9
+ // they tabbed onto a button.
10
+ function useEscapeKey(onEscape) {
11
+ useEffect(() => {
12
+ const handler = (e) => {
13
+ if (e.key === 'Escape') {
14
+ e.preventDefault();
15
+ onEscape();
16
+ }
17
+ };
18
+ document.addEventListener('keydown', handler);
19
+ return () => document.removeEventListener('keydown', handler);
20
+ }, [onEscape]);
21
+ }
22
+ const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
23
+ // Constrain Tab / Shift+Tab to cycle within ``container``. Without this, Tab
24
+ // from the last button in the modal escapes into the lab toolbar — keyboard
25
+ // users lose the dialog. ARIA APG's modal pattern requires the trap.
26
+ function useFocusTrap(container) {
27
+ useEffect(() => {
28
+ const node = container.current;
29
+ if (!node) {
30
+ return;
31
+ }
32
+ const handler = (e) => {
33
+ if (e.key !== 'Tab') {
34
+ return;
35
+ }
36
+ const focusables = Array.from(node.querySelectorAll(FOCUSABLE_SELECTOR)).filter(el => !el.hasAttribute('disabled'));
37
+ if (focusables.length === 0) {
38
+ return;
39
+ }
40
+ const first = focusables[0];
41
+ const last = focusables[focusables.length - 1];
42
+ const active = document.activeElement;
43
+ if (e.shiftKey && active === first) {
44
+ e.preventDefault();
45
+ last.focus();
46
+ }
47
+ else if (!e.shiftKey && active === last) {
48
+ e.preventDefault();
49
+ first.focus();
50
+ }
51
+ };
52
+ node.addEventListener('keydown', handler);
53
+ return () => node.removeEventListener('keydown', handler);
54
+ }, [container]);
55
+ }
56
+ // Must match SKILL_NAME_PATTERN in notebook_intelligence/skillset.py
57
+ const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
58
+ const SKILL_NAME_REQUIREMENT = 'Must be lowercase letters, digits, or hyphens (starting with a letter or digit), max 64 chars.';
59
+ const SKILL_ENTRY_FILE = 'SKILL.md';
60
+ // Bundles with more files than this collapse their file tabs to just SKILL.md.
61
+ // Keeps the tab strip usable for reference-data skills that ship hundreds of
62
+ // helper files; those are easier to edit on disk anyway.
63
+ const BUNDLE_FILE_DISPLAY_LIMIT = 20;
64
+ const COMMON_TOOLS = [
65
+ 'Read',
66
+ 'Write',
67
+ 'Edit',
68
+ 'Bash',
69
+ 'Glob',
70
+ 'Grep',
71
+ 'Task',
72
+ 'TodoWrite',
73
+ 'WebFetch',
74
+ 'WebSearch',
75
+ 'NotebookEdit'
76
+ ];
77
+ export function SettingsPanelComponentSkills(_props) {
78
+ const [skills, setSkills] = useState([]);
79
+ const [context, setContext] = useState(null);
80
+ const [loading, setLoading] = useState(true);
81
+ const [error, setError] = useState(null);
82
+ const [view, setView] = useState({ kind: 'list' });
83
+ const [prompt, setPrompt] = useState(null);
84
+ const [undo, setUndo] = useState(null);
85
+ const [importOpen, setImportOpen] = useState(false);
86
+ const [syncing, setSyncing] = useState(false);
87
+ const [syncMessage, setSyncMessage] = useState(null);
88
+ const [allowGithubImport, setAllowGithubImport] = useState(NBIAPI.config.allowGithubSkillImport);
89
+ const hasManagedSkills = skills.some(s => s.managed);
90
+ const hasTrackingSkills = skills.some(s => s.tracksUpstream);
91
+ const refresh = async () => {
92
+ var _a;
93
+ setLoading(true);
94
+ setError(null);
95
+ try {
96
+ const list = await NBIAPI.listSkills();
97
+ setSkills(list);
98
+ }
99
+ catch (e) {
100
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
101
+ }
102
+ finally {
103
+ setLoading(false);
104
+ }
105
+ };
106
+ useEffect(() => {
107
+ refresh();
108
+ NBIAPI.getSkillsContext()
109
+ .then(setContext)
110
+ .catch(() => {
111
+ // Non-fatal — panel still works without the context hints.
112
+ });
113
+ const listener = () => {
114
+ refresh();
115
+ };
116
+ const configListener = () => {
117
+ setAllowGithubImport(NBIAPI.config.allowGithubSkillImport);
118
+ };
119
+ NBIAPI.skillsReloaded.connect(listener);
120
+ NBIAPI.configChanged.connect(configListener);
121
+ return () => {
122
+ NBIAPI.skillsReloaded.disconnect(listener);
123
+ NBIAPI.configChanged.disconnect(configListener);
124
+ };
125
+ }, []);
126
+ const undoRef = useRef(null);
127
+ undoRef.current = undo;
128
+ const dismissUndo = () => {
129
+ setUndo(prev => {
130
+ if (prev) {
131
+ window.clearTimeout(prev.timerId);
132
+ }
133
+ return null;
134
+ });
135
+ };
136
+ useEffect(() => {
137
+ return () => {
138
+ if (undoRef.current) {
139
+ window.clearTimeout(undoRef.current.timerId);
140
+ }
141
+ };
142
+ }, []);
143
+ const handleSyncManaged = async () => {
144
+ var _a;
145
+ setSyncing(true);
146
+ setSyncMessage(null);
147
+ setError(null);
148
+ try {
149
+ const r = await NBIAPI.reconcileManagedSkills();
150
+ const summary = `Sync complete. ${r.added} added, ${r.updated} updated, ${r.removed} removed, ${r.unchanged} unchanged.`;
151
+ setSyncMessage(r.errors.length ? `${summary} (${r.errors.length} error(s))` : summary);
152
+ if (r.errors.length) {
153
+ setError(r.errors.join('\n'));
154
+ }
155
+ await refresh();
156
+ }
157
+ catch (e) {
158
+ setError(`Sync failed: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
159
+ }
160
+ finally {
161
+ setSyncing(false);
162
+ }
163
+ };
164
+ const handleSyncTracking = async (skill) => {
165
+ var _a;
166
+ setSyncing(true);
167
+ setSyncMessage(null);
168
+ setError(null);
169
+ try {
170
+ const r = await NBIAPI.syncTrackingSkill(skill.scope, skill.name);
171
+ setSyncMessage(r.updated
172
+ ? `Synced "${skill.name}" to ${r.ref.slice(0, 7)}.`
173
+ : `"${skill.name}" already up to date at ${r.ref.slice(0, 7)}.`);
174
+ await refresh();
175
+ }
176
+ catch (e) {
177
+ setError(`Sync failed for "${skill.name}": ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
178
+ }
179
+ finally {
180
+ setSyncing(false);
181
+ }
182
+ };
183
+ const handleSyncAllTracking = async () => {
184
+ var _a;
185
+ setSyncing(true);
186
+ setSyncMessage(null);
187
+ setError(null);
188
+ try {
189
+ const results = await NBIAPI.syncAllTrackingSkills();
190
+ const updated = results.filter(r => r.updated).length;
191
+ const unchanged = results.filter(r => r.updated === false).length;
192
+ const errors = results.filter(r => r.error);
193
+ const summary = `Sync complete. ${updated} updated, ${unchanged} unchanged, ${errors.length} error(s).`;
194
+ setSyncMessage(summary);
195
+ if (errors.length) {
196
+ setError(errors.map(r => `${r.name}: ${r.error}`).join('\n'));
197
+ }
198
+ await refresh();
199
+ }
200
+ catch (e) {
201
+ setError(`Sync failed: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
202
+ }
203
+ finally {
204
+ setSyncing(false);
205
+ }
206
+ };
207
+ const handleDelete = async (skill) => {
208
+ var _a, _b, _c;
209
+ const result = await showDialog({
210
+ title: 'Delete skill?',
211
+ body: `"${skill.name}" will be deleted.`,
212
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Delete' })]
213
+ });
214
+ if (!result.button.accept) {
215
+ return;
216
+ }
217
+ // Snapshot the full bundle (SKILL.md + helper files) *before* deleting so the Undo
218
+ // toast can recreate it byte-for-byte. The backend only exposes a shallow delete
219
+ // API, so restoration is the client's job.
220
+ let detail;
221
+ try {
222
+ detail = await NBIAPI.readSkill(skill.scope, skill.name);
223
+ }
224
+ catch (e) {
225
+ setError(`Failed to read skill: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
226
+ return;
227
+ }
228
+ const pathsToSnapshot = ((_b = detail.files) !== null && _b !== void 0 ? _b : []).filter(p => p !== SKILL_ENTRY_FILE);
229
+ const snapshots = await Promise.all(pathsToSnapshot.map(async (path) => {
230
+ try {
231
+ const content = await NBIAPI.readBundleFile(skill.scope, skill.name, path);
232
+ return { path, content };
233
+ }
234
+ catch (_a) {
235
+ // Best-effort snapshot — skip unreadable files.
236
+ return null;
237
+ }
238
+ }));
239
+ const bundleFiles = snapshots.filter((s) => s !== null);
240
+ try {
241
+ await NBIAPI.deleteSkill(skill.scope, skill.name);
242
+ await refresh();
243
+ }
244
+ catch (e) {
245
+ setError(`Failed to delete skill: ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`);
246
+ return;
247
+ }
248
+ dismissUndo();
249
+ const timerId = window.setTimeout(() => {
250
+ setUndo(null);
251
+ }, 8000);
252
+ setUndo({ detail, bundleFiles, timerId });
253
+ };
254
+ const handleUndoDelete = async () => {
255
+ var _a;
256
+ if (!undo) {
257
+ return;
258
+ }
259
+ const { detail, bundleFiles } = undo;
260
+ dismissUndo();
261
+ try {
262
+ await NBIAPI.createSkill({
263
+ scope: detail.scope,
264
+ name: detail.name,
265
+ description: detail.description,
266
+ allowedTools: detail.allowedTools,
267
+ body: detail.body
268
+ });
269
+ await Promise.all(bundleFiles.map(({ path, content }) => NBIAPI.writeBundleFile(detail.scope, detail.name, path, content).catch(() => {
270
+ // Non-fatal — skill is restored even if a bundle file fails.
271
+ })));
272
+ await refresh();
273
+ }
274
+ catch (e) {
275
+ setError(`Failed to restore skill: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
276
+ }
277
+ };
278
+ const handleRename = (skill) => {
279
+ setPrompt({ kind: 'rename', skill });
280
+ };
281
+ const handleDuplicate = (skill) => {
282
+ setPrompt({ kind: 'duplicate', skill });
283
+ };
284
+ const commitRename = async (skill, newName) => {
285
+ await NBIAPI.renameSkill(skill.scope, skill.name, newName);
286
+ setPrompt(null);
287
+ await refresh();
288
+ };
289
+ const commitDuplicate = async (skill, targetScope, newName) => {
290
+ const detail = await NBIAPI.readSkill(skill.scope, skill.name);
291
+ await NBIAPI.createSkill({
292
+ scope: targetScope,
293
+ name: newName,
294
+ description: detail.description,
295
+ allowedTools: detail.allowedTools,
296
+ body: detail.body
297
+ });
298
+ const filesToCopy = detail.files.filter(f => f !== SKILL_ENTRY_FILE);
299
+ await Promise.all(filesToCopy.map(async (file) => {
300
+ const content = await NBIAPI.readBundleFile(skill.scope, skill.name, file);
301
+ await NBIAPI.writeBundleFile(targetScope, newName, file, content);
302
+ }));
303
+ setPrompt(null);
304
+ await refresh();
305
+ };
306
+ if (view.kind === 'editor') {
307
+ return (React.createElement(SkillEditor, { scope: view.scope, name: view.name, onClose: async () => {
308
+ await refresh();
309
+ setView({ kind: 'list' });
310
+ } }));
311
+ }
312
+ const userSkills = skills.filter(s => s.scope === 'user');
313
+ const projectSkills = skills.filter(s => s.scope === 'project');
314
+ return (React.createElement("div", { className: "config-dialog-body nbi-skills-panel" },
315
+ React.createElement("div", { className: "nbi-skills-header" },
316
+ React.createElement("div", { className: "nbi-skills-title" }, "Skills"),
317
+ React.createElement("div", { className: "nbi-skills-header-actions" },
318
+ hasManagedSkills && (React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: handleSyncManaged, disabled: syncing, title: "Reconcile managed skills against the org manifest" },
319
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, syncing ? 'Syncing…' : 'Sync managed skills'))),
320
+ hasTrackingSkills && allowGithubImport && (React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: handleSyncAllTracking, disabled: syncing, title: "Re-fetch every skill set to track upstream from GitHub" },
321
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, syncing ? 'Syncing…' : 'Sync tracking skills'))),
322
+ allowGithubImport && (React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => setImportOpen(true), title: "Import from GitHub" },
323
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, hasManagedSkills ? 'Import' : 'Import from GitHub'))),
324
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => setView({ kind: 'editor', scope: 'user', name: null }) },
325
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "New Skill")))),
326
+ !NBIAPI.config.isInClaudeCodeMode && (React.createElement("div", { className: "nbi-skills-info-banner", role: "note" }, "Skills are consumed by Claude Code. Enable Claude mode in the Claude settings tab to use skills you author here.")),
327
+ syncMessage && (React.createElement("div", { className: "nbi-skills-sync-message", role: "status" }, syncMessage)),
328
+ error && (React.createElement("div", { className: "nbi-skills-error", role: "alert" }, error)),
329
+ React.createElement(SkillScopeSection, { scope: "user", label: "USER", pathHint: (context === null || context === void 0 ? void 0 : context.userSkillsDir) || '~/.claude/skills/', skills: userSkills, loading: loading, onEdit: s => setView({ kind: 'editor', scope: s.scope, name: s.name }), onNew: () => setView({ kind: 'editor', scope: 'user', name: null }), onRename: handleRename, onDuplicate: handleDuplicate, onDelete: handleDelete, onSync: handleSyncTracking, syncDisabled: syncing || !allowGithubImport }),
330
+ React.createElement(SkillScopeSection, { scope: "project", label: (context === null || context === void 0 ? void 0 : context.projectName) ? `PROJECT · ${context.projectName}` : 'PROJECT', pathHint: (context === null || context === void 0 ? void 0 : context.projectSkillsDir) || '<project>/.claude/skills/', skills: projectSkills, loading: loading, onEdit: s => setView({ kind: 'editor', scope: s.scope, name: s.name }), onNew: () => setView({ kind: 'editor', scope: 'project', name: null }), onRename: handleRename, onDuplicate: handleDuplicate, onDelete: handleDelete, onSync: handleSyncTracking, syncDisabled: syncing || !allowGithubImport }),
331
+ prompt && (React.createElement(SkillPromptDialog, { prompt: prompt, existingNames: skills, onCancel: () => setPrompt(null), onRename: commitRename, onDuplicate: commitDuplicate })),
332
+ undo && (React.createElement(UndoToast, { message: `Deleted "${undo.detail.name}"`, onUndo: handleUndoDelete, onDismiss: dismissUndo })),
333
+ importOpen && (React.createElement(GitHubImportDialog, { onCancel: () => setImportOpen(false), onImported: async () => {
334
+ setImportOpen(false);
335
+ await refresh();
336
+ } }))));
337
+ }
338
+ function GitHubImportDialog(props) {
339
+ const [step, setStep] = useState('url');
340
+ const [url, setUrl] = useState('');
341
+ const [scope, setScope] = useState('user');
342
+ const [preview, setPreview] = useState(null);
343
+ const [nameOverride, setNameOverride] = useState('');
344
+ const [overwrite, setOverwrite] = useState(false);
345
+ const [tracksUpstream, setTracksUpstream] = useState(false);
346
+ const [busy, setBusy] = useState(false);
347
+ const [error, setError] = useState(null);
348
+ useEscapeKey(props.onCancel);
349
+ const formRef = useRef(null);
350
+ useFocusTrap(formRef);
351
+ const effectiveName = (nameOverride.trim() || (preview === null || preview === void 0 ? void 0 : preview.name) || '').trim();
352
+ const nameValid = SKILL_NAME_PATTERN.test(effectiveName);
353
+ const collides = preview !== null &&
354
+ ((scope === 'user' && preview.existsInUserScope) ||
355
+ (scope === 'project' && preview.existsInProjectScope)) &&
356
+ effectiveName === preview.name;
357
+ const canFetchPreview = !busy && url.trim().length > 0;
358
+ const canInstall = !busy && preview !== null && nameValid && (!collides || overwrite);
359
+ const handleFetchPreview = async (e) => {
360
+ var _a;
361
+ e.preventDefault();
362
+ if (!canFetchPreview) {
363
+ return;
364
+ }
365
+ setBusy(true);
366
+ setError(null);
367
+ try {
368
+ const p = await NBIAPI.previewSkillImport(url.trim());
369
+ setPreview(p);
370
+ setNameOverride('');
371
+ setOverwrite(false);
372
+ setStep('preview');
373
+ }
374
+ catch (err) {
375
+ setError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
376
+ }
377
+ finally {
378
+ setBusy(false);
379
+ }
380
+ };
381
+ const handleInstall = async (e) => {
382
+ var _a;
383
+ e.preventDefault();
384
+ if (!canInstall || !preview) {
385
+ return;
386
+ }
387
+ setBusy(true);
388
+ setError(null);
389
+ try {
390
+ await NBIAPI.importSkill({
391
+ url: url.trim(),
392
+ scope,
393
+ name: effectiveName !== preview.name ? effectiveName : undefined,
394
+ overwrite: collides ? true : undefined,
395
+ tracksUpstream: tracksUpstream || undefined
396
+ });
397
+ await props.onImported();
398
+ }
399
+ catch (err) {
400
+ setError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
401
+ setBusy(false);
402
+ }
403
+ };
404
+ return (React.createElement("div", { className: "nbi-modal-backdrop", onClick: props.onCancel, role: "presentation" },
405
+ React.createElement("form", { ref: formRef, className: "nbi-modal-card", role: "dialog", "aria-modal": "true", "aria-label": "Import skill from GitHub", onClick: e => e.stopPropagation(), onSubmit: step === 'url' ? handleFetchPreview : handleInstall },
406
+ React.createElement("div", { className: "nbi-modal-title" }, "Import skill from GitHub"),
407
+ React.createElement("div", { className: "nbi-modal-body" },
408
+ step === 'url' && (React.createElement(React.Fragment, null,
409
+ React.createElement("div", { className: "nbi-form-field" },
410
+ React.createElement("label", { htmlFor: "nbi-import-url" }, "GitHub repo URL"),
411
+ React.createElement("input", { id: "nbi-import-url", type: "text", autoFocus: true, value: url, onChange: e => setUrl(e.target.value), placeholder: "https://github.com/owner/repo or .../tree/main/path/to/skill" }),
412
+ React.createElement("div", { className: "nbi-form-hint" },
413
+ "Link to the repo root, a branch, or a subdirectory containing",
414
+ ' ',
415
+ React.createElement("code", null, "SKILL.md"),
416
+ ". Private repos work if",
417
+ ' ',
418
+ React.createElement("code", null, "GITHUB_TOKEN"),
419
+ " is set or ",
420
+ React.createElement("code", null, "gh auth login"),
421
+ ' ',
422
+ "is configured on the Jupyter server.")),
423
+ React.createElement("div", { className: "nbi-form-field" },
424
+ React.createElement("label", { htmlFor: "nbi-import-scope" }, "Install into"),
425
+ React.createElement("select", { id: "nbi-import-scope", value: scope, onChange: e => setScope(e.target.value) },
426
+ React.createElement("option", { value: "user" }, "User (available in all projects)"),
427
+ React.createElement("option", { value: "project" }, "Project (this project only)"))))),
428
+ step === 'preview' && preview && (React.createElement(React.Fragment, null,
429
+ React.createElement("div", { className: "nbi-import-preview" },
430
+ React.createElement("div", { className: "nbi-import-preview-row" },
431
+ React.createElement("span", { className: "nbi-import-preview-label" }, "Name"),
432
+ React.createElement("span", { className: "nbi-import-preview-value" }, preview.name)),
433
+ preview.description && (React.createElement("div", { className: "nbi-import-preview-row" },
434
+ React.createElement("span", { className: "nbi-import-preview-label" }, "Description"),
435
+ React.createElement("span", { className: "nbi-import-preview-value" }, preview.description))),
436
+ preview.allowedTools.length > 0 && (React.createElement("div", { className: "nbi-import-preview-row" },
437
+ React.createElement("span", { className: "nbi-import-preview-label" }, "Allowed tools"),
438
+ React.createElement("span", { className: "nbi-import-preview-value" }, preview.allowedTools.join(', ')))),
439
+ React.createElement("div", { className: "nbi-import-preview-row" },
440
+ React.createElement("span", { className: "nbi-import-preview-label" }, "Files"),
441
+ React.createElement("span", { className: "nbi-import-preview-value" },
442
+ "SKILL.md",
443
+ preview.files.length > 0 &&
444
+ ` + ${preview.files.length} other${preview.files.length === 1 ? '' : 's'}`)),
445
+ React.createElement("div", { className: "nbi-import-preview-row" },
446
+ React.createElement("span", { className: "nbi-import-preview-label" }, "Source"),
447
+ React.createElement("span", { className: "nbi-import-preview-value nbi-import-preview-source" }, preview.canonicalUrl))),
448
+ React.createElement("div", { className: "nbi-form-field" },
449
+ React.createElement("label", { htmlFor: "nbi-import-name" }, "Name (optional override)"),
450
+ React.createElement("input", { id: "nbi-import-name", type: "text", value: nameOverride, onChange: e => setNameOverride(e.target.value), placeholder: preview.name, "aria-invalid": effectiveName.length > 0 && !nameValid ? true : undefined }),
451
+ effectiveName.length > 0 && !nameValid && (React.createElement("div", { className: "nbi-form-field-error" }, SKILL_NAME_REQUIREMENT))),
452
+ collides && (React.createElement("div", { className: "nbi-form-field" },
453
+ React.createElement("label", { className: "nbi-checkbox-label" },
454
+ React.createElement("input", { type: "checkbox", checked: overwrite, onChange: e => setOverwrite(e.target.checked) }),
455
+ "Overwrite existing ",
456
+ scope,
457
+ " skill \"",
458
+ preview.name,
459
+ "\""))),
460
+ React.createElement("div", { className: "nbi-form-field" },
461
+ React.createElement("label", { className: "nbi-checkbox-label" },
462
+ React.createElement("input", { type: "checkbox", checked: tracksUpstream, onChange: e => setTracksUpstream(e.target.checked) }),
463
+ "Track upstream. Show a Sync button so I can re-pull this skill from GitHub later"))))),
464
+ error && (React.createElement("div", { className: "nbi-skills-error", role: "alert" }, error)),
465
+ React.createElement("div", { className: "nbi-modal-actions" },
466
+ step === 'preview' && (React.createElement("button", { type: "button", className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => {
467
+ setStep('url');
468
+ setPreview(null);
469
+ setError(null);
470
+ } },
471
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Back"))),
472
+ React.createElement("button", { type: "button", className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: props.onCancel },
473
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Cancel")),
474
+ step === 'url' ? (React.createElement("button", { type: "submit", className: "jp-Dialog-button jp-mod-accept jp-mod-styled", disabled: !canFetchPreview },
475
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, busy ? 'Fetching…' : 'Next'))) : (React.createElement("button", { type: "submit", className: "jp-Dialog-button jp-mod-accept jp-mod-styled", disabled: !canInstall },
476
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, busy ? 'Installing…' : 'Install')))))));
477
+ }
478
+ function SkillScopeSection(props) {
479
+ return (React.createElement("div", { className: "nbi-skills-section" },
480
+ React.createElement("div", { className: "nbi-skills-section-caption", title: props.pathHint },
481
+ props.label,
482
+ " \u00B7 ",
483
+ props.skills.length),
484
+ props.loading && props.skills.length === 0 && (React.createElement("div", { className: "nbi-skills-empty" }, "Loading\u2026")),
485
+ !props.loading && props.skills.length === 0 && (React.createElement("div", { className: "nbi-skills-empty" },
486
+ React.createElement("span", null,
487
+ "No ",
488
+ props.scope,
489
+ " skills. They live in ",
490
+ React.createElement("code", null, props.pathHint),
491
+ "."),
492
+ React.createElement("button", { className: "jp-toast-button jp-mod-small jp-Button", onClick: props.onNew },
493
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" },
494
+ "+ New ",
495
+ props.scope,
496
+ " skill")))),
497
+ props.skills.map(skill => (React.createElement(SkillRow, { key: `${skill.scope}:${skill.name}`, skill: skill, onEdit: () => props.onEdit(skill), onRename: () => props.onRename(skill), onDuplicate: () => props.onDuplicate(skill), onDelete: () => props.onDelete(skill), onSync: () => props.onSync(skill), syncDisabled: props.syncDisabled })))));
498
+ }
499
+ function ManagedBadge(props) {
500
+ var _a;
501
+ return (React.createElement("span", { className: "nbi-skill-managed-badge", title: `Managed by org manifest (${(_a = props.source) !== null && _a !== void 0 ? _a : ''})` }, "Managed"));
502
+ }
503
+ function TrackingBadge(props) {
504
+ const refSuffix = props.ref ? ` (last sync: ${props.ref.slice(0, 7)})` : '';
505
+ return (React.createElement("span", { className: "nbi-skill-tracking-badge", title: `Tracking upstream from ${props.source}${refSuffix}` }, "Tracking"));
506
+ }
507
+ function SkillRow(props) {
508
+ const { skill } = props;
509
+ const stopAnd = (fn) => (e) => {
510
+ e.stopPropagation();
511
+ fn();
512
+ };
513
+ // The wrapper div is no longer interactive. Nested interactive widgets
514
+ // (role=button on a div that contains <button> children) violate ARIA
515
+ // semantics and confuse screen readers and form serialisation. The
516
+ // visible Edit button is the canonical activation path; the wrapper
517
+ // keeps its click handler purely as a sighted-user convenience so a
518
+ // click anywhere on the row still opens the editor, but it's no longer
519
+ // in the keyboard tab order — that role belongs to the explicit
520
+ // buttons below.
521
+ return (React.createElement("div", { className: "nbi-skill-row", onClick: props.onEdit },
522
+ React.createElement("div", { className: "nbi-skill-row-main" },
523
+ React.createElement("div", { className: "nbi-skill-row-name" },
524
+ skill.name,
525
+ skill.managed && React.createElement(ManagedBadge, { source: skill.managedSource }),
526
+ !skill.managed && skill.tracksUpstream && (React.createElement(TrackingBadge, { source: skill.source, ref: skill.trackingRef }))),
527
+ skill.description && (React.createElement("div", { className: "nbi-skill-row-description" }, skill.description))),
528
+ React.createElement("div", { className: "nbi-skill-row-actions", onClick: e => e.stopPropagation() },
529
+ !skill.managed && skill.tracksUpstream && (React.createElement("button", { type: "button", className: "nbi-icon-button", "aria-label": "Sync skill from GitHub", title: "Sync from GitHub", onClick: stopAnd(props.onSync), disabled: props.syncDisabled }, "\u21BB")),
530
+ React.createElement("button", { type: "button", className: "nbi-icon-button", "aria-label": skill.managed ? 'View skill' : 'Edit skill', title: skill.managed ? 'View (managed, read-only)' : 'Edit', onClick: stopAnd(props.onEdit) },
531
+ React.createElement(VscEdit, { "aria-hidden": "true" })),
532
+ !skill.managed && (React.createElement("button", { type: "button", className: "nbi-icon-button", "aria-label": "Rename skill", title: "Rename", onClick: stopAnd(props.onRename) },
533
+ React.createElement("span", { "aria-hidden": "true", className: "nbi-icon-button-text" }, "Aa"))),
534
+ React.createElement("button", { type: "button", className: "nbi-icon-button", "aria-label": "Duplicate skill", title: "Duplicate", onClick: stopAnd(props.onDuplicate) },
535
+ React.createElement(VscCopy, { "aria-hidden": "true" })),
536
+ !skill.managed && (React.createElement("button", { type: "button", className: "nbi-icon-button danger", "aria-label": "Delete skill", title: "Delete", onClick: stopAnd(props.onDelete) },
537
+ React.createElement(VscTrash, { "aria-hidden": "true" }))))));
538
+ }
539
+ function SkillPromptDialog(props) {
540
+ const { prompt } = props;
541
+ const isRename = prompt.kind === 'rename';
542
+ const initialName = isRename
543
+ ? prompt.skill.name
544
+ : `${prompt.skill.name}-copy`;
545
+ const initialScope = isRename
546
+ ? prompt.skill.scope
547
+ : prompt.skill.scope === 'user'
548
+ ? 'project'
549
+ : 'user';
550
+ const [name, setName] = useState(initialName);
551
+ const [scope, setScope] = useState(initialScope);
552
+ const [error, setError] = useState(null);
553
+ const [busy, setBusy] = useState(false);
554
+ useEscapeKey(props.onCancel);
555
+ const formRef = useRef(null);
556
+ useFocusTrap(formRef);
557
+ const trimmed = name.trim();
558
+ const nameValid = SKILL_NAME_PATTERN.test(trimmed);
559
+ const isUnchangedRename = isRename && trimmed === prompt.skill.name;
560
+ const conflict = props.existingNames.some(s => s.scope === scope && s.name === trimmed && !isUnchangedRename);
561
+ const canSubmit = !busy && nameValid && !conflict && !isUnchangedRename;
562
+ const title = isRename ? 'Rename skill' : 'Duplicate skill';
563
+ const submitLabel = isRename ? 'Rename' : 'Duplicate';
564
+ const handleSubmit = async (e) => {
565
+ var _a;
566
+ e.preventDefault();
567
+ if (!canSubmit) {
568
+ return;
569
+ }
570
+ setBusy(true);
571
+ setError(null);
572
+ try {
573
+ if (isRename) {
574
+ await props.onRename(prompt.skill, trimmed);
575
+ }
576
+ else {
577
+ await props.onDuplicate(prompt.skill, scope, trimmed);
578
+ }
579
+ }
580
+ catch (err) {
581
+ setError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
582
+ setBusy(false);
583
+ }
584
+ };
585
+ return (React.createElement("div", { className: "nbi-modal-backdrop", onClick: props.onCancel, role: "presentation" },
586
+ React.createElement("form", { ref: formRef, className: "nbi-modal-card", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: e => e.stopPropagation(), onSubmit: handleSubmit },
587
+ React.createElement("div", { className: "nbi-modal-title" }, title),
588
+ React.createElement("div", { className: "nbi-modal-body" },
589
+ !isRename && (React.createElement("div", { className: "nbi-form-field" },
590
+ React.createElement("label", { htmlFor: "nbi-dup-scope" }, "Target scope"),
591
+ React.createElement("select", { id: "nbi-dup-scope", value: scope, onChange: e => setScope(e.target.value) },
592
+ React.createElement("option", { value: "user" }, "User"),
593
+ React.createElement("option", { value: "project" }, "Project")))),
594
+ React.createElement("div", { className: "nbi-form-field" },
595
+ React.createElement("label", { htmlFor: "nbi-prompt-name" }, "New name"),
596
+ React.createElement("input", { id: "nbi-prompt-name", type: "text", autoFocus: true, value: name, onChange: e => setName(e.target.value), onKeyDown: e => {
597
+ if (e.key === 'Escape') {
598
+ e.preventDefault();
599
+ props.onCancel();
600
+ }
601
+ }, "aria-invalid": (!nameValid && trimmed.length > 0) || conflict
602
+ ? true
603
+ : undefined }),
604
+ trimmed.length > 0 && !nameValid && (React.createElement("div", { className: "nbi-form-field-error" }, SKILL_NAME_REQUIREMENT)),
605
+ conflict && (React.createElement("div", { className: "nbi-form-field-error" },
606
+ "A ",
607
+ scope,
608
+ " skill named \"",
609
+ trimmed,
610
+ "\" already exists.")))),
611
+ error && (React.createElement("div", { className: "nbi-skills-error", role: "alert" }, error)),
612
+ React.createElement("div", { className: "nbi-modal-actions" },
613
+ React.createElement("button", { type: "button", className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: props.onCancel },
614
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Cancel")),
615
+ React.createElement("button", { type: "submit", className: "jp-Dialog-button jp-mod-accept jp-mod-styled", disabled: !canSubmit },
616
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, busy ? 'Working…' : submitLabel))))));
617
+ }
618
+ function UndoToast(props) {
619
+ return (React.createElement("div", { className: "nbi-undo-toast", role: "status" },
620
+ React.createElement("span", { className: "nbi-undo-toast-message" }, props.message),
621
+ React.createElement("button", { type: "button", className: "nbi-undo-toast-action", onClick: props.onUndo }, "Undo"),
622
+ React.createElement("button", { type: "button", className: "nbi-undo-toast-close", "aria-label": "Dismiss", onClick: props.onDismiss }, "\u00D7")));
623
+ }
624
+ function SkillEditor(props) {
625
+ var _a, _b, _c, _d;
626
+ const isNew = props.name === null;
627
+ const [scope, setScope] = useState(props.scope);
628
+ const [name, setName] = useState((_a = props.name) !== null && _a !== void 0 ? _a : '');
629
+ const [description, setDescription] = useState('');
630
+ const [allowedTools, setAllowedTools] = useState([]);
631
+ const [savedMeta, setSavedMeta] = useState({
632
+ description: '',
633
+ allowedTools: []
634
+ });
635
+ // Bundle files are lazy-loaded when the user switches to their tab (see the fetch effect
636
+ // below). `loaded: false` means we know the file exists on disk but haven't fetched its
637
+ // content yet. SKILL.md is always loaded eagerly in loadSkill().
638
+ const [buffers, setBuffers] = useState(new Map([[SKILL_ENTRY_FILE, { content: '', saved: '', loaded: true }]]));
639
+ const orderedFileList = useMemo(() => [
640
+ SKILL_ENTRY_FILE,
641
+ ...Array.from(buffers.keys())
642
+ .filter(f => f !== SKILL_ENTRY_FILE)
643
+ .sort()
644
+ ], [buffers]);
645
+ const bundleOverflow = orderedFileList.length > BUNDLE_FILE_DISPLAY_LIMIT;
646
+ const displayedFileList = bundleOverflow
647
+ ? [SKILL_ENTRY_FILE]
648
+ : orderedFileList;
649
+ const [activeFile, setActiveFile] = useState(SKILL_ENTRY_FILE);
650
+ // If a reload pushes the bundle over the threshold while a helper file is
651
+ // selected, snap back to SKILL.md — otherwise the tab strip shows no active
652
+ // tab and there's no way to navigate anywhere else.
653
+ useEffect(() => {
654
+ if (bundleOverflow && activeFile !== SKILL_ENTRY_FILE) {
655
+ setActiveFile(SKILL_ENTRY_FILE);
656
+ }
657
+ }, [bundleOverflow, activeFile]);
658
+ const [renaming, setRenaming] = useState(null);
659
+ const [renameDraft, setRenameDraft] = useState('');
660
+ const [addFileDraft, setAddFileDraft] = useState('');
661
+ const [loading, setLoading] = useState(!isNew);
662
+ const [saving, setSaving] = useState(false);
663
+ const [error, setError] = useState(null);
664
+ const [hasCreated, setHasCreated] = useState(false);
665
+ const [managed, setManaged] = useState(false);
666
+ const [managedSource, setManagedSource] = useState('');
667
+ const [tracksUpstream, setTracksUpstream] = useState(false);
668
+ const [trackingRef, setTrackingRef] = useState('');
669
+ const [skillSource, setSkillSource] = useState('');
670
+ const [togglingTracking, setTogglingTracking] = useState(false);
671
+ const [rootPath, setRootPath] = useState('');
672
+ const errorRef = useRef(null);
673
+ const effectiveName = isNew && !hasCreated ? name : ((_b = props.name) !== null && _b !== void 0 ? _b : name);
674
+ const effectiveIsNew = isNew && !hasCreated;
675
+ const loadSkill = async (s, n) => {
676
+ var _a, _b, _c, _d, _e, _f, _g, _h;
677
+ const skill = await NBIAPI.readSkill(s, n);
678
+ setDescription(skill.description);
679
+ setAllowedTools((_a = skill.allowedTools) !== null && _a !== void 0 ? _a : []);
680
+ setManaged(skill.managed);
681
+ setManagedSource((_b = skill.managedSource) !== null && _b !== void 0 ? _b : '');
682
+ setTracksUpstream(skill.tracksUpstream);
683
+ setTrackingRef((_c = skill.trackingRef) !== null && _c !== void 0 ? _c : '');
684
+ setSkillSource((_d = skill.source) !== null && _d !== void 0 ? _d : '');
685
+ setRootPath((_e = skill.rootPath) !== null && _e !== void 0 ? _e : '');
686
+ const skillMdBody = (_f = skill.body) !== null && _f !== void 0 ? _f : '';
687
+ setSavedMeta({
688
+ description: skill.description,
689
+ allowedTools: (_g = skill.allowedTools) !== null && _g !== void 0 ? _g : []
690
+ });
691
+ const newBuffers = new Map();
692
+ newBuffers.set(SKILL_ENTRY_FILE, {
693
+ content: skillMdBody,
694
+ saved: skillMdBody,
695
+ loaded: true
696
+ });
697
+ for (const file of (_h = skill.files) !== null && _h !== void 0 ? _h : []) {
698
+ if (file !== SKILL_ENTRY_FILE) {
699
+ newBuffers.set(file, { content: '', saved: '', loaded: false });
700
+ }
701
+ }
702
+ setBuffers(newBuffers);
703
+ setActiveFile(SKILL_ENTRY_FILE);
704
+ };
705
+ useEffect(() => {
706
+ if (isNew) {
707
+ return;
708
+ }
709
+ let cancelled = false;
710
+ (async () => {
711
+ var _a;
712
+ try {
713
+ await loadSkill(props.scope, props.name);
714
+ }
715
+ catch (e) {
716
+ if (!cancelled) {
717
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
718
+ }
719
+ }
720
+ finally {
721
+ if (!cancelled) {
722
+ setLoading(false);
723
+ }
724
+ }
725
+ })();
726
+ return () => {
727
+ cancelled = true;
728
+ };
729
+ }, [isNew, props.scope, props.name]);
730
+ useEffect(() => {
731
+ if (isNew && !hasCreated) {
732
+ return;
733
+ }
734
+ const onReload = () => {
735
+ var _a;
736
+ const skillName = (_a = props.name) !== null && _a !== void 0 ? _a : (hasCreated ? name : null);
737
+ if (!skillName) {
738
+ return;
739
+ }
740
+ loadSkill(props.scope, skillName).catch((e) => {
741
+ var _a;
742
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
743
+ });
744
+ };
745
+ NBIAPI.skillsReloaded.connect(onReload);
746
+ return () => {
747
+ NBIAPI.skillsReloaded.disconnect(onReload);
748
+ };
749
+ }, [isNew, hasCreated, props.scope, props.name, name]);
750
+ useEffect(() => {
751
+ var _a;
752
+ if (error) {
753
+ (_a = errorRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
754
+ }
755
+ }, [error]);
756
+ const metaDirty = description !== savedMeta.description ||
757
+ allowedTools.length !== savedMeta.allowedTools.length ||
758
+ allowedTools.some((t, i) => t !== savedMeta.allowedTools[i]);
759
+ const fileDirty = (path) => {
760
+ const b = buffers.get(path);
761
+ return b !== undefined && b.content !== b.saved;
762
+ };
763
+ const anyFileDirty = Array.from(buffers.keys()).some(fileDirty);
764
+ const anyDirty = metaDirty || anyFileDirty;
765
+ const nameValid = SKILL_NAME_PATTERN.test(name);
766
+ const descriptionValid = description.trim().length > 0;
767
+ const canSave = !saving && !managed && (!effectiveIsNew || nameValid) && descriptionValid;
768
+ const updateBuffer = (path, content) => {
769
+ setBuffers(prev => {
770
+ var _a;
771
+ const next = new Map(prev);
772
+ const existing = (_a = next.get(path)) !== null && _a !== void 0 ? _a : {
773
+ content: '',
774
+ saved: '',
775
+ loaded: true
776
+ };
777
+ next.set(path, { ...existing, content });
778
+ return next;
779
+ });
780
+ };
781
+ const currentBuffer = (_d = (_c = buffers.get(activeFile)) === null || _c === void 0 ? void 0 : _c.content) !== null && _d !== void 0 ? _d : '';
782
+ const handleSave = async () => {
783
+ var _a, _b, _c, _d, _e, _f, _g;
784
+ setError(null);
785
+ if (effectiveIsNew && !nameValid) {
786
+ setError(`Invalid name. ${SKILL_NAME_REQUIREMENT}`);
787
+ return;
788
+ }
789
+ setSaving(true);
790
+ try {
791
+ if (effectiveIsNew) {
792
+ const initialBody = (_b = (_a = buffers.get(SKILL_ENTRY_FILE)) === null || _a === void 0 ? void 0 : _a.content) !== null && _b !== void 0 ? _b : '';
793
+ await NBIAPI.createSkill({
794
+ scope,
795
+ name,
796
+ description,
797
+ allowedTools,
798
+ body: initialBody
799
+ });
800
+ const extraFiles = Array.from(buffers.entries()).filter(([p]) => p !== SKILL_ENTRY_FILE);
801
+ for (const [path, buf] of extraFiles) {
802
+ try {
803
+ await NBIAPI.writeBundleFile(scope, name, path, buf.content);
804
+ }
805
+ catch (e) {
806
+ setError(`${path}: ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : String(e)}`);
807
+ }
808
+ }
809
+ setHasCreated(true);
810
+ await loadSkill(scope, name);
811
+ }
812
+ else {
813
+ const skillName = effectiveName;
814
+ const dirtyFilePaths = Array.from(buffers.keys()).filter(p => p !== SKILL_ENTRY_FILE && fileDirty(p));
815
+ const skillMdBody = (_e = (_d = buffers.get(SKILL_ENTRY_FILE)) === null || _d === void 0 ? void 0 : _d.content) !== null && _e !== void 0 ? _e : '';
816
+ const skillMdDirty = fileDirty(SKILL_ENTRY_FILE);
817
+ const savedPaths = new Set();
818
+ const fileResults = await Promise.allSettled(dirtyFilePaths.map(p => NBIAPI.writeBundleFile(scope, skillName, p, buffers.get(p).content)));
819
+ const errors = [];
820
+ fileResults.forEach((result, i) => {
821
+ var _a, _b;
822
+ const p = dirtyFilePaths[i];
823
+ if (result.status === 'fulfilled') {
824
+ savedPaths.add(p);
825
+ }
826
+ else {
827
+ errors.push(`${p}: ${(_b = (_a = result.reason) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : result.reason}`);
828
+ }
829
+ });
830
+ let metaSaved = !(metaDirty || skillMdDirty);
831
+ if (metaDirty || skillMdDirty) {
832
+ try {
833
+ await NBIAPI.updateSkill(scope, skillName, {
834
+ description,
835
+ allowedTools,
836
+ body: skillMdBody
837
+ });
838
+ metaSaved = true;
839
+ if (skillMdDirty) {
840
+ savedPaths.add(SKILL_ENTRY_FILE);
841
+ }
842
+ }
843
+ catch (e) {
844
+ errors.push((_f = e === null || e === void 0 ? void 0 : e.message) !== null && _f !== void 0 ? _f : String(e));
845
+ }
846
+ }
847
+ setBuffers(prev => {
848
+ const next = new Map(prev);
849
+ for (const path of savedPaths) {
850
+ const b = next.get(path);
851
+ if (b) {
852
+ next.set(path, {
853
+ content: b.content,
854
+ saved: b.content,
855
+ loaded: true
856
+ });
857
+ }
858
+ }
859
+ return next;
860
+ });
861
+ if (metaSaved) {
862
+ setSavedMeta({ description, allowedTools });
863
+ }
864
+ if (errors.length > 0) {
865
+ setError(errors.join('\n'));
866
+ }
867
+ }
868
+ }
869
+ catch (e) {
870
+ setError((_g = e === null || e === void 0 ? void 0 : e.message) !== null && _g !== void 0 ? _g : String(e));
871
+ }
872
+ finally {
873
+ setSaving(false);
874
+ }
875
+ };
876
+ const handleBack = async () => {
877
+ if (anyDirty) {
878
+ const result = await showDialog({
879
+ title: 'Discard unsaved changes?',
880
+ body: 'This skill has unsaved changes. Discard them?',
881
+ buttons: [
882
+ Dialog.cancelButton({ label: 'Keep editing' }),
883
+ Dialog.warnButton({ label: 'Discard' })
884
+ ]
885
+ });
886
+ if (!result.button.accept) {
887
+ return;
888
+ }
889
+ }
890
+ props.onClose();
891
+ };
892
+ const handleSelectFile = (path) => {
893
+ if (path === activeFile) {
894
+ return;
895
+ }
896
+ setActiveFile(path);
897
+ };
898
+ const handleAddFile = async () => {
899
+ var _a;
900
+ const relPath = addFileDraft.trim();
901
+ if (!relPath) {
902
+ return;
903
+ }
904
+ if (buffers.has(relPath)) {
905
+ setError(`"${relPath}" already exists in this bundle.`);
906
+ return;
907
+ }
908
+ try {
909
+ if (!effectiveIsNew) {
910
+ await NBIAPI.writeBundleFile(scope, effectiveName, relPath, '');
911
+ }
912
+ setBuffers(prev => {
913
+ const next = new Map(prev);
914
+ // For new skills, mark as dirty (saved='' vs content='' — equal, so not dirty;
915
+ // but the initial empty file write happens on skill creation save).
916
+ next.set(relPath, { content: '', saved: '', loaded: true });
917
+ return next;
918
+ });
919
+ setActiveFile(relPath);
920
+ setAddFileDraft('');
921
+ }
922
+ catch (e) {
923
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
924
+ }
925
+ };
926
+ const handleDeleteFile = async (path) => {
927
+ var _a;
928
+ const result = await showDialog({
929
+ title: 'Delete file?',
930
+ body: `"${path}" will be permanently deleted.`,
931
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Delete' })]
932
+ });
933
+ if (!result.button.accept) {
934
+ return;
935
+ }
936
+ try {
937
+ if (!effectiveIsNew) {
938
+ await NBIAPI.deleteBundleFile(scope, effectiveName, path);
939
+ }
940
+ setBuffers(prev => {
941
+ const next = new Map(prev);
942
+ next.delete(path);
943
+ return next;
944
+ });
945
+ if (activeFile === path) {
946
+ setActiveFile(SKILL_ENTRY_FILE);
947
+ }
948
+ }
949
+ catch (e) {
950
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
951
+ }
952
+ };
953
+ const handleBeginRename = (path) => {
954
+ setRenaming(path);
955
+ setRenameDraft(path);
956
+ };
957
+ const handleCommitRename = async () => {
958
+ var _a;
959
+ if (renaming === null) {
960
+ return;
961
+ }
962
+ const newPath = renameDraft.trim();
963
+ if (!newPath || newPath === renaming) {
964
+ setRenaming(null);
965
+ return;
966
+ }
967
+ try {
968
+ if (!effectiveIsNew) {
969
+ await NBIAPI.renameBundleFile(scope, effectiveName, renaming, newPath);
970
+ }
971
+ setBuffers(prev => {
972
+ const next = new Map(prev);
973
+ const b = next.get(renaming);
974
+ if (b) {
975
+ next.set(newPath, b);
976
+ next.delete(renaming);
977
+ }
978
+ return next;
979
+ });
980
+ if (activeFile === renaming) {
981
+ setActiveFile(newPath);
982
+ }
983
+ setRenaming(null);
984
+ setRenameDraft('');
985
+ }
986
+ catch (e) {
987
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
988
+ setRenaming(null);
989
+ }
990
+ };
991
+ useEffect(() => {
992
+ if (effectiveIsNew || activeFile === SKILL_ENTRY_FILE) {
993
+ return;
994
+ }
995
+ const existing = buffers.get(activeFile);
996
+ if (existing === null || existing === void 0 ? void 0 : existing.loaded) {
997
+ return;
998
+ }
999
+ let cancelled = false;
1000
+ (async () => {
1001
+ var _a;
1002
+ try {
1003
+ const content = await NBIAPI.readBundleFile(scope, effectiveName, activeFile);
1004
+ if (cancelled) {
1005
+ return;
1006
+ }
1007
+ setBuffers(prev => {
1008
+ const existing = prev.get(activeFile);
1009
+ // Don't clobber user edits that happened while the fetch was in flight.
1010
+ if (existing === null || existing === void 0 ? void 0 : existing.loaded) {
1011
+ return prev;
1012
+ }
1013
+ const next = new Map(prev);
1014
+ next.set(activeFile, { content, saved: content, loaded: true });
1015
+ return next;
1016
+ });
1017
+ }
1018
+ catch (e) {
1019
+ if (!cancelled) {
1020
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
1021
+ }
1022
+ }
1023
+ })();
1024
+ return () => {
1025
+ cancelled = true;
1026
+ };
1027
+ }, [activeFile, effectiveIsNew, scope, effectiveName]);
1028
+ const nameError = effectiveIsNew && name && !nameValid ? SKILL_NAME_REQUIREMENT : null;
1029
+ const descriptionError = !descriptionValid && (description.length > 0 || !effectiveIsNew)
1030
+ ? 'Description is required.'
1031
+ : null;
1032
+ const editingSkillMd = activeFile === SKILL_ENTRY_FILE;
1033
+ const bodyLanguageHint = editingSkillMd ? 'markdown' : activeFile;
1034
+ return (React.createElement("form", { className: "nbi-skill-editor", onSubmit: e => {
1035
+ e.preventDefault();
1036
+ if (canSave && (effectiveIsNew || anyDirty)) {
1037
+ handleSave();
1038
+ }
1039
+ } },
1040
+ React.createElement("div", { className: "nbi-skill-editor-header" },
1041
+ React.createElement("nav", { className: "nbi-skill-editor-breadcrumb", "aria-label": "Breadcrumb" },
1042
+ React.createElement("button", { type: "button", className: "nbi-breadcrumb-link", onClick: handleBack }, "Skills"),
1043
+ React.createElement("span", { className: "nbi-breadcrumb-separator", "aria-hidden": "true" }, "/"),
1044
+ React.createElement("span", { className: "nbi-breadcrumb-current" },
1045
+ effectiveIsNew ? 'New skill' : effectiveName,
1046
+ managed && React.createElement(ManagedBadge, { source: managedSource }))),
1047
+ React.createElement("div", { className: "nbi-skill-editor-actions" },
1048
+ React.createElement("button", { type: "button", className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: handleBack },
1049
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, managed ? 'Close' : 'Cancel')),
1050
+ !managed && (React.createElement("button", { type: "submit", className: "jp-Dialog-button jp-mod-accept jp-mod-styled", disabled: !canSave || (!effectiveIsNew && !anyDirty) },
1051
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" },
1052
+ saving ? 'Saving…' : 'Save',
1053
+ anyDirty && (React.createElement("span", { className: "nbi-dirty-marker", "aria-label": "Unsaved changes" },
1054
+ ' ',
1055
+ "\u2022"))))))),
1056
+ managed && (React.createElement("div", { className: "nbi-skills-managed-banner", role: "note" }, "This skill is managed by the organization manifest and is read-only. Changes will be overwritten on the next sync.")),
1057
+ !managed && !effectiveIsNew && skillSource && (React.createElement("div", { className: "nbi-skills-tracking-row" },
1058
+ React.createElement("label", { className: "nbi-checkbox-label" },
1059
+ React.createElement("input", { type: "checkbox", checked: tracksUpstream, disabled: togglingTracking || saving, onChange: async (e) => {
1060
+ var _a;
1061
+ const next = e.target.checked;
1062
+ // Optimistic flip so the checkbox tracks the click while
1063
+ // the PUT is in flight; rolled back on failure below.
1064
+ setTracksUpstream(next);
1065
+ setTogglingTracking(true);
1066
+ setError(null);
1067
+ try {
1068
+ const updated = await NBIAPI.updateSkill(scope, effectiveName, { tracksUpstream: next });
1069
+ setTracksUpstream(updated.tracksUpstream);
1070
+ setTrackingRef(updated.trackingRef);
1071
+ }
1072
+ catch (err) {
1073
+ // Roll back the optimistic flip so the UI matches the
1074
+ // server's state (the server kept the prior value).
1075
+ setTracksUpstream(!next);
1076
+ setError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
1077
+ }
1078
+ finally {
1079
+ setTogglingTracking(false);
1080
+ }
1081
+ } }),
1082
+ "Track upstream"),
1083
+ React.createElement("span", { className: "nbi-form-hint" },
1084
+ "Source: ",
1085
+ skillSource,
1086
+ tracksUpstream &&
1087
+ trackingRef &&
1088
+ ` · last sync: ${trackingRef.slice(0, 7)}`))),
1089
+ loading ? (React.createElement("div", { className: "nbi-skill-editor-loading" }, "Loading skill\u2026")) : (React.createElement(React.Fragment, null,
1090
+ error && (React.createElement("div", { className: "nbi-skills-error", role: "alert", ref: errorRef }, error)),
1091
+ React.createElement("div", { className: "nbi-skill-editor-meta" },
1092
+ React.createElement("div", { className: "nbi-form-row" },
1093
+ React.createElement("div", { className: "nbi-form-row-inline" },
1094
+ React.createElement("div", { className: "nbi-form-field" },
1095
+ React.createElement("label", null, "Scope"),
1096
+ React.createElement("select", { value: scope, disabled: !effectiveIsNew, onChange: e => setScope(e.target.value) },
1097
+ React.createElement("option", { value: "user" }, "User"),
1098
+ React.createElement("option", { value: "project" }, "Project"))),
1099
+ React.createElement("div", { className: "nbi-form-field" },
1100
+ React.createElement("label", null, "Name"),
1101
+ React.createElement("input", { type: "text", value: name, disabled: !effectiveIsNew, onChange: e => setName(e.target.value), placeholder: "my-skill-name", "aria-invalid": nameError ? true : undefined }),
1102
+ nameError && (React.createElement("div", { className: "nbi-form-field-error" }, nameError)))),
1103
+ !effectiveIsNew && (React.createElement("div", { className: "nbi-form-hint" }, "Scope and name are set at creation and can't be changed. Delete and recreate to change them."))),
1104
+ React.createElement("div", { className: "nbi-form-field nbi-form-field-wide" },
1105
+ React.createElement("label", null, "Description"),
1106
+ React.createElement("textarea", { rows: 3, value: description, onChange: e => setDescription(e.target.value), placeholder: "What this skill does, shown to Claude when deciding whether to use it", "aria-invalid": descriptionError ? true : undefined, required: true }),
1107
+ descriptionError ? (React.createElement("div", { className: "nbi-form-field-error" }, descriptionError)) : (React.createElement("div", { className: "nbi-form-hint" }, "Required. Claude uses this to decide when to apply the skill."))),
1108
+ React.createElement("div", { className: "nbi-form-field nbi-form-field-wide" },
1109
+ React.createElement("label", null, "Allowed tools"),
1110
+ React.createElement(AllowedToolsPicker, { value: allowedTools, onChange: setAllowedTools }),
1111
+ React.createElement("div", { className: "nbi-form-hint" },
1112
+ "Quick-add pills cover common tools. Type patterns like",
1113
+ ' ',
1114
+ React.createElement("code", null, "Bash(git:*)"),
1115
+ " or ",
1116
+ React.createElement("code", null, "Read(./docs/**)"),
1117
+ " for finer-grained permissions."))),
1118
+ bundleOverflow && (React.createElement("div", { className: "nbi-form-hint" },
1119
+ "Bundle contains ",
1120
+ orderedFileList.length,
1121
+ " files \u2014 showing",
1122
+ ' ',
1123
+ SKILL_ENTRY_FILE,
1124
+ " only. Edit supporting files directly on disk.",
1125
+ rootPath && (React.createElement(React.Fragment, null,
1126
+ ' ',
1127
+ "Path: ",
1128
+ React.createElement("code", null, rootPath))))),
1129
+ React.createElement(BundleFileTabs, { files: displayedFileList, activeFile: activeFile, renaming: renaming, renameDraft: renameDraft, addFileDraft: addFileDraft, fileDirty: fileDirty, canAddFiles: !bundleOverflow, onSelect: handleSelectFile, onBeginRename: handleBeginRename, onCommitRename: handleCommitRename, onCancelRename: () => setRenaming(null), onRenameDraftChange: setRenameDraft, onDelete: handleDeleteFile, onAddFileDraftChange: setAddFileDraft, onAddFile: handleAddFile }),
1130
+ React.createElement("div", { className: "nbi-skill-editor-body" },
1131
+ React.createElement("div", { className: "nbi-skill-editor-pane" },
1132
+ React.createElement(AutoGrowTextarea, { value: currentBuffer, onChange: v => updateBuffer(activeFile, v), minRows: 18, languageHint: bodyLanguageHint })))))));
1133
+ }
1134
+ function BundleFileTabs(props) {
1135
+ const [openMenu, setOpenMenu] = useState(null);
1136
+ const [adding, setAdding] = useState(false);
1137
+ useEffect(() => {
1138
+ if (!openMenu) {
1139
+ return;
1140
+ }
1141
+ const onDocClick = () => setOpenMenu(null);
1142
+ document.addEventListener('click', onDocClick);
1143
+ return () => document.removeEventListener('click', onDocClick);
1144
+ }, [openMenu]);
1145
+ const commitAdd = () => {
1146
+ if (props.addFileDraft.trim()) {
1147
+ props.onAddFile();
1148
+ }
1149
+ setAdding(false);
1150
+ };
1151
+ return (React.createElement("div", { className: "nbi-skill-editor-tabs", role: "tablist" },
1152
+ props.files.map(file => {
1153
+ const active = file === props.activeFile;
1154
+ const dirty = props.fileDirty(file);
1155
+ const isRenaming = props.renaming === file;
1156
+ const canModify = file !== SKILL_ENTRY_FILE;
1157
+ return (React.createElement("div", { key: file, role: "tab", "aria-selected": active, className: `nbi-skill-editor-tab${active ? ' active' : ''}`, onClick: () => !isRenaming && props.onSelect(file) }, isRenaming ? (React.createElement("input", { type: "text", autoFocus: true, value: props.renameDraft, onChange: e => props.onRenameDraftChange(e.target.value), onKeyDown: e => {
1158
+ if (e.key === 'Enter') {
1159
+ e.preventDefault();
1160
+ props.onCommitRename();
1161
+ }
1162
+ else if (e.key === 'Escape') {
1163
+ e.preventDefault();
1164
+ props.onCancelRename();
1165
+ }
1166
+ }, onBlur: props.onCancelRename, onClick: e => e.stopPropagation(), className: "nbi-skill-editor-tab-rename-input" })) : (React.createElement(React.Fragment, null,
1167
+ React.createElement("span", { className: "nbi-skill-editor-tab-label" },
1168
+ file,
1169
+ dirty && React.createElement("span", { className: "nbi-dirty-marker" }, " \u2022")),
1170
+ canModify && (React.createElement("div", { className: "nbi-skill-editor-tab-kebab-wrap", onClick: e => e.stopPropagation() },
1171
+ React.createElement("button", { type: "button", className: "nbi-icon-button", "aria-label": `Actions for ${file}`, "aria-haspopup": "menu", "aria-expanded": openMenu === file, onClick: e => {
1172
+ e.stopPropagation();
1173
+ setOpenMenu(openMenu === file ? null : file);
1174
+ } }, "\u22EF"),
1175
+ openMenu === file && (React.createElement("div", { className: "nbi-skill-editor-tab-menu", role: "menu" },
1176
+ React.createElement("button", { type: "button", role: "menuitem", onClick: () => {
1177
+ setOpenMenu(null);
1178
+ props.onBeginRename(file);
1179
+ } }, "Rename"),
1180
+ React.createElement("button", { type: "button", role: "menuitem", className: "danger", onClick: () => {
1181
+ setOpenMenu(null);
1182
+ props.onDelete(file);
1183
+ } }, "Delete")))))))));
1184
+ }),
1185
+ props.canAddFiles &&
1186
+ (adding ? (React.createElement("div", { className: "nbi-skill-editor-tab adding" },
1187
+ React.createElement("input", { type: "text", autoFocus: true, placeholder: "new-file.md", value: props.addFileDraft, onChange: e => props.onAddFileDraftChange(e.target.value), onKeyDown: e => {
1188
+ if (e.key === 'Enter') {
1189
+ e.preventDefault();
1190
+ commitAdd();
1191
+ }
1192
+ else if (e.key === 'Escape') {
1193
+ e.preventDefault();
1194
+ props.onAddFileDraftChange('');
1195
+ setAdding(false);
1196
+ }
1197
+ }, onBlur: commitAdd, className: "nbi-skill-editor-tab-rename-input" }))) : (React.createElement("button", { type: "button", className: "nbi-skill-editor-tab-add", "aria-label": "Add file", title: "Add file", onClick: () => setAdding(true) }, "+")))));
1198
+ }
1199
+ function AllowedToolsPicker(props) {
1200
+ const [draft, setDraft] = useState('');
1201
+ const commit = (raw) => {
1202
+ const parts = raw
1203
+ .split(',')
1204
+ .map(t => t.trim())
1205
+ .filter(t => t.length > 0 && !props.value.includes(t));
1206
+ if (parts.length === 0) {
1207
+ setDraft('');
1208
+ return;
1209
+ }
1210
+ props.onChange([...props.value, ...parts]);
1211
+ setDraft('');
1212
+ };
1213
+ const toggle = (tool) => {
1214
+ if (props.value.includes(tool)) {
1215
+ props.onChange(props.value.filter(t => t !== tool));
1216
+ }
1217
+ else {
1218
+ props.onChange([...props.value, tool]);
1219
+ }
1220
+ };
1221
+ const remove = (tool) => {
1222
+ props.onChange(props.value.filter(t => t !== tool));
1223
+ };
1224
+ const handleKeyDown = (e) => {
1225
+ if (e.key === 'Enter' || e.key === ',') {
1226
+ if (draft.trim()) {
1227
+ e.preventDefault();
1228
+ commit(draft);
1229
+ }
1230
+ }
1231
+ else if (e.key === 'Backspace' && !draft && props.value.length > 0) {
1232
+ remove(props.value[props.value.length - 1]);
1233
+ }
1234
+ };
1235
+ return (React.createElement("div", null,
1236
+ React.createElement("div", { className: "nbi-tools-picker-input" },
1237
+ props.value.map(tool => (React.createElement("span", { key: tool, className: "pill-item checked" },
1238
+ tool,
1239
+ React.createElement("button", { onClick: () => remove(tool), "aria-label": `Remove ${tool}`, className: "nbi-pill-remove" }, "\u00D7")))),
1240
+ React.createElement("input", { type: "text", value: draft, onChange: e => setDraft(e.target.value), onKeyDown: handleKeyDown, onBlur: () => draft.trim() && commit(draft), placeholder: props.value.length === 0 ? 'e.g. Bash(git:*)' : '' })),
1241
+ React.createElement("div", { className: "nbi-tools-picker-suggestions" }, COMMON_TOOLS.map(tool => (React.createElement("span", { key: tool, className: `pill-item${props.value.includes(tool) ? ' checked' : ''}`, onClick: () => toggle(tool) }, tool))))));
1242
+ }
1243
+ function AutoGrowTextarea(props) {
1244
+ const ref = useRef(null);
1245
+ const lineHeightRef = useRef(null);
1246
+ useLayoutEffect(() => {
1247
+ const ta = ref.current;
1248
+ if (!ta) {
1249
+ return;
1250
+ }
1251
+ if (lineHeightRef.current === null) {
1252
+ const computed = window.getComputedStyle(ta);
1253
+ const parsed = parseFloat(computed.lineHeight);
1254
+ // CSS "normal" resolves to NaN here; 1.4× font-size is a standard approximation.
1255
+ lineHeightRef.current = Number.isNaN(parsed)
1256
+ ? parseFloat(computed.fontSize) * 1.4
1257
+ : parsed;
1258
+ }
1259
+ ta.style.height = 'auto';
1260
+ const next = Math.max(ta.scrollHeight, props.minRows * lineHeightRef.current);
1261
+ ta.style.height = `${next}px`;
1262
+ }, [props.value, props.minRows]);
1263
+ return (React.createElement("textarea", { ref: ref, className: "nbi-skill-editor-textarea", value: props.value, onChange: e => props.onChange(e.target.value), spellCheck: false, "data-language": props.languageHint }));
1264
+ }