@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,39 @@
1
+ export declare namespace CommandIDs {
2
+ const chatuserInput = "notebook-intelligence:chat-user-input";
3
+ const insertAtCursor = "notebook-intelligence:insert-at-cursor";
4
+ const addCodeAsNewCell = "notebook-intelligence:add-code-as-new-cell";
5
+ const createNewFile = "notebook-intelligence:create-new-file";
6
+ const createNewNotebookFromPython = "notebook-intelligence:create-new-notebook-from-py";
7
+ const renameNotebook = "notebook-intelligence:rename-notebook";
8
+ const addCodeCellToNotebook = "notebook-intelligence:add-code-cell-to-notebook";
9
+ const addMarkdownCellToNotebook = "notebook-intelligence:add-markdown-cell-to-notebook";
10
+ const editorGenerateCode = "notebook-intelligence:editor-generate-code";
11
+ const editorExplainThisCode = "notebook-intelligence:editor-explain-this-code";
12
+ const editorFixThisCode = "notebook-intelligence:editor-fix-this-code";
13
+ const editorExplainThisOutput = "notebook-intelligence:editor-explain-this-output";
14
+ const editorTroubleshootThisOutput = "notebook-intelligence:editor-troubleshoot-this-output";
15
+ const editorAskAboutThisOutput = "notebook-intelligence:editor-ask-about-this-output";
16
+ const openGitHubCopilotLoginDialog = "notebook-intelligence:open-github-copilot-login-dialog";
17
+ const openConfigurationDialog = "notebook-intelligence:open-configuration-dialog";
18
+ const addMarkdownCellToActiveNotebook = "notebook-intelligence:add-markdown-cell-to-active-notebook";
19
+ const addCodeCellToActiveNotebook = "notebook-intelligence:add-code-cell-to-active-notebook";
20
+ const deleteCellAtIndex = "notebook-intelligence:delete-cell-at-index";
21
+ const insertCellAtIndex = "notebook-intelligence:insert-cell-at-index";
22
+ const getCellTypeAndSource = "notebook-intelligence:get-cell-type-and-source";
23
+ const setCellTypeAndSource = "notebook-intelligence:set-cell-type-and-source";
24
+ const getNumberOfCells = "notebook-intelligence:get-number-of-cells";
25
+ const getCellOutput = "notebook-intelligence:get-cell-output";
26
+ const runCellAtIndex = "notebook-intelligence:run-cell-at-index";
27
+ const getCurrentFileContent = "notebook-intelligence:get-current-file-content";
28
+ const setCurrentFileContent = "notebook-intelligence:set-current-file-content";
29
+ const openMCPConfigEditor = "notebook-intelligence:open-mcp-config-editor";
30
+ const showFormInputDialog = "notebook-intelligence:show-form-input-dialog";
31
+ const runCommandInTerminal = "notebook-intelligence:run-command-in-terminal";
32
+ const openClaudeCodeLauncher = "notebook-intelligence:open-claude-code-launcher";
33
+ const openOpenCodeLauncher = "notebook-intelligence:open-opencode-launcher";
34
+ const openPiLauncher = "notebook-intelligence:open-pi-launcher";
35
+ const openGitHubCopilotCliLauncher = "notebook-intelligence:open-github-copilot-cli-launcher";
36
+ const openCodexLauncher = "notebook-intelligence:open-codex-launcher";
37
+ const showTour = "notebook-intelligence:show-tour";
38
+ const focusChatInput = "notebook-intelligence:focus-chat-input";
39
+ }
@@ -0,0 +1,44 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+ // Centralized command-id constants for the JupyterLab plugin. Hoisted out
3
+ // of `src/index.ts` so non-JL modules (and Jest tests) can import them
4
+ // without pulling the JupyterLab packages into their dependency graph.
5
+ export var CommandIDs;
6
+ (function (CommandIDs) {
7
+ CommandIDs.chatuserInput = 'notebook-intelligence:chat-user-input';
8
+ CommandIDs.insertAtCursor = 'notebook-intelligence:insert-at-cursor';
9
+ CommandIDs.addCodeAsNewCell = 'notebook-intelligence:add-code-as-new-cell';
10
+ CommandIDs.createNewFile = 'notebook-intelligence:create-new-file';
11
+ CommandIDs.createNewNotebookFromPython = 'notebook-intelligence:create-new-notebook-from-py';
12
+ CommandIDs.renameNotebook = 'notebook-intelligence:rename-notebook';
13
+ CommandIDs.addCodeCellToNotebook = 'notebook-intelligence:add-code-cell-to-notebook';
14
+ CommandIDs.addMarkdownCellToNotebook = 'notebook-intelligence:add-markdown-cell-to-notebook';
15
+ CommandIDs.editorGenerateCode = 'notebook-intelligence:editor-generate-code';
16
+ CommandIDs.editorExplainThisCode = 'notebook-intelligence:editor-explain-this-code';
17
+ CommandIDs.editorFixThisCode = 'notebook-intelligence:editor-fix-this-code';
18
+ CommandIDs.editorExplainThisOutput = 'notebook-intelligence:editor-explain-this-output';
19
+ CommandIDs.editorTroubleshootThisOutput = 'notebook-intelligence:editor-troubleshoot-this-output';
20
+ CommandIDs.editorAskAboutThisOutput = 'notebook-intelligence:editor-ask-about-this-output';
21
+ CommandIDs.openGitHubCopilotLoginDialog = 'notebook-intelligence:open-github-copilot-login-dialog';
22
+ CommandIDs.openConfigurationDialog = 'notebook-intelligence:open-configuration-dialog';
23
+ CommandIDs.addMarkdownCellToActiveNotebook = 'notebook-intelligence:add-markdown-cell-to-active-notebook';
24
+ CommandIDs.addCodeCellToActiveNotebook = 'notebook-intelligence:add-code-cell-to-active-notebook';
25
+ CommandIDs.deleteCellAtIndex = 'notebook-intelligence:delete-cell-at-index';
26
+ CommandIDs.insertCellAtIndex = 'notebook-intelligence:insert-cell-at-index';
27
+ CommandIDs.getCellTypeAndSource = 'notebook-intelligence:get-cell-type-and-source';
28
+ CommandIDs.setCellTypeAndSource = 'notebook-intelligence:set-cell-type-and-source';
29
+ CommandIDs.getNumberOfCells = 'notebook-intelligence:get-number-of-cells';
30
+ CommandIDs.getCellOutput = 'notebook-intelligence:get-cell-output';
31
+ CommandIDs.runCellAtIndex = 'notebook-intelligence:run-cell-at-index';
32
+ CommandIDs.getCurrentFileContent = 'notebook-intelligence:get-current-file-content';
33
+ CommandIDs.setCurrentFileContent = 'notebook-intelligence:set-current-file-content';
34
+ CommandIDs.openMCPConfigEditor = 'notebook-intelligence:open-mcp-config-editor';
35
+ CommandIDs.showFormInputDialog = 'notebook-intelligence:show-form-input-dialog';
36
+ CommandIDs.runCommandInTerminal = 'notebook-intelligence:run-command-in-terminal';
37
+ CommandIDs.openClaudeCodeLauncher = 'notebook-intelligence:open-claude-code-launcher';
38
+ CommandIDs.openOpenCodeLauncher = 'notebook-intelligence:open-opencode-launcher';
39
+ CommandIDs.openPiLauncher = 'notebook-intelligence:open-pi-launcher';
40
+ CommandIDs.openGitHubCopilotCliLauncher = 'notebook-intelligence:open-github-copilot-cli-launcher';
41
+ CommandIDs.openCodexLauncher = 'notebook-intelligence:open-codex-launcher';
42
+ CommandIDs.showTour = 'notebook-intelligence:show-tour';
43
+ CommandIDs.focusChatInput = 'notebook-intelligence:focus-chat-input';
44
+ })(CommandIDs || (CommandIDs = {}));
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare function AskUserQuestion(props: any): React.JSX.Element;
@@ -0,0 +1,85 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+ import React, { useId, useState } from 'react';
3
+ export function AskUserQuestion(props) {
4
+ var _a;
5
+ const userQuestions = props.userQuestions.content;
6
+ const [selectedAnswers, setSelectedAnswers] = useState({});
7
+ // Form-scoped id prefix so DOM ids stay unique even when two questions
8
+ // share label text (or several AskUserQuestion forms render in the
9
+ // same chat transcript). React.useId() is the purpose-built primitive
10
+ // here: stable for the component's lifetime, SSR-safe, and survives
11
+ // StrictMode double-render without producing mismatched id/htmlFor
12
+ // pairs (a Math.random fallback could mis-pair under cache eviction).
13
+ // The server-provided identifier is preferred when present so two
14
+ // remounts of the same form keep the same DOM ids.
15
+ const reactId = useId();
16
+ const serverId = (_a = userQuestions.identifier) === null || _a === void 0 ? void 0 : _a.id;
17
+ const formIdPrefix = typeof serverId === 'string' && serverId.length > 0
18
+ ? `nbi-auq-${serverId}`
19
+ : `nbi-auq${reactId}`;
20
+ const onOptionSelected = (question, option) => {
21
+ var _a, _b, _c;
22
+ if (question.multiSelect) {
23
+ if ((_a = selectedAnswers[question.question]) === null || _a === void 0 ? void 0 : _a.includes(option.label)) {
24
+ setSelectedAnswers({
25
+ ...selectedAnswers,
26
+ [question.question]: ((_b = selectedAnswers[question.question]) !== null && _b !== void 0 ? _b : []).filter((o) => o !== option.label)
27
+ });
28
+ }
29
+ else {
30
+ setSelectedAnswers({
31
+ ...selectedAnswers,
32
+ [question.question]: [
33
+ ...((_c = selectedAnswers[question.question]) !== null && _c !== void 0 ? _c : []),
34
+ option.label
35
+ ]
36
+ });
37
+ }
38
+ }
39
+ else {
40
+ setSelectedAnswers({
41
+ ...selectedAnswers,
42
+ [question.question]: [option.label]
43
+ });
44
+ }
45
+ };
46
+ return (React.createElement(React.Fragment, null,
47
+ userQuestions.title ? (React.createElement("div", null,
48
+ React.createElement("b", null, userQuestions.title))) : null,
49
+ userQuestions.message ? React.createElement("div", null, userQuestions.message) : null,
50
+ React.createElement("form", { className: "ask-user-question-form", onSubmit: event => {
51
+ event.preventDefault();
52
+ props.onSubmit(selectedAnswers);
53
+ } },
54
+ userQuestions.questions.map((question, qIndex) => {
55
+ const questionDomId = `${formIdPrefix}-q${qIndex}`;
56
+ // A single-select group is a radio group with a shared name so
57
+ // screen readers announce "1 of N selected" rather than
58
+ // treating each option as an independent checkbox. The wrapper
59
+ // role mirrors the input type: radiogroup for radios, group
60
+ // (the ARIA-1.2 fallback when no dedicated checkbox-group role
61
+ // exists) for checkboxes.
62
+ const inputType = question.multiSelect ? 'checkbox' : 'radio';
63
+ const groupRole = question.multiSelect ? 'group' : 'radiogroup';
64
+ return (React.createElement("div", { className: "ask-user-question-container", key: questionDomId, role: groupRole, "aria-labelledby": `${questionDomId}-label` },
65
+ React.createElement("div", { className: "ask-user-question-question", id: `${questionDomId}-label` }, question.question),
66
+ React.createElement("div", { className: "ask-user-question-header" }, question.header),
67
+ React.createElement("div", { className: "ask-user-question-options" }, question.options.map((option, oIndex) => {
68
+ var _a, _b;
69
+ const optionDomId = `${questionDomId}-o${oIndex}`;
70
+ return (React.createElement("div", { className: "ask-user-question-option", key: optionDomId },
71
+ React.createElement("div", { className: "ask-user-question-option-input-container" },
72
+ React.createElement("input", { id: optionDomId, name: questionDomId, type: inputType, checked: (_b = (_a = selectedAnswers[question.question]) === null || _a === void 0 ? void 0 : _a.includes(option.label)) !== null && _b !== void 0 ? _b : false, onChange: () => onOptionSelected(question, option) }),
73
+ React.createElement("label", { htmlFor: optionDomId, className: "ask-user-question-option-label-container" },
74
+ React.createElement("div", { className: "ask-user-question-option-label" }, option.label),
75
+ React.createElement("div", { className: "ask-user-question-option-description" }, option.description)))));
76
+ }))));
77
+ }),
78
+ React.createElement("div", { className: "ask-user-question-footer" },
79
+ React.createElement("button", { type: "submit", className: "jp-Dialog-button jp-mod-accept jp-mod-styled" },
80
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, userQuestions.submitLabel)),
81
+ React.createElement("button", { type: "button", className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => {
82
+ props.onCancel();
83
+ } },
84
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, userQuestions.cancelLabel))))));
85
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare function CheckBoxItem(props: any): React.JSX.Element;
@@ -0,0 +1,30 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+ import React from 'react';
3
+ import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from '../icons';
4
+ export function CheckBoxItem(props) {
5
+ const indent = props.indent || 0;
6
+ const disabled = props.disabled || false;
7
+ const checked = !!props.checked;
8
+ const activate = (event) => {
9
+ if (!disabled) {
10
+ props.onClick(event);
11
+ }
12
+ };
13
+ // Custom checkbox widget. Tracks the WAI-ARIA pattern: role='checkbox'
14
+ // + aria-checked + tabIndex=0 + Space/Enter to toggle. Disabled state
15
+ // is exposed via aria-disabled so the element stays focusable; a
16
+ // screen reader user can still announce "checkbox, disabled" and hear
17
+ // what's off without being able to toggle it.
18
+ return (React.createElement("div", { className: `checkbox-item checkbox-item-indent-${indent} ${props.header ? 'checkbox-item-header' : ''}`, title: props.tooltip || props.title || '', role: "checkbox", "aria-checked": checked, "aria-disabled": disabled || undefined, tabIndex: disabled ? -1 : 0, onClick: activate, onKeyDown: event => {
19
+ if (event.key === ' ' || event.key === 'Enter') {
20
+ // Space scrolls the page by default, Enter is a no-op on a
21
+ // <div>; prevent both so the checkbox can own them.
22
+ event.preventDefault();
23
+ activate(event);
24
+ }
25
+ } },
26
+ React.createElement("div", { className: "checkbox-item-toggle" },
27
+ checked ? (React.createElement(MdCheckBox, { className: "checkbox-icon", style: { opacity: disabled ? 0.5 : 1 }, "aria-hidden": "true" })) : (React.createElement(MdOutlineCheckBoxOutlineBlank, { className: "checkbox-icon", style: { opacity: disabled ? 0.5 : 1 }, "aria-hidden": "true" })),
28
+ React.createElement("span", { style: { opacity: disabled ? 0.5 : 1 } }, props.label)),
29
+ props.title && (React.createElement("div", { className: "checkbox-item-description" }, props.title))));
30
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="react" />
2
+ export declare function SettingsPanelComponentClaudeMCP(_props: any): JSX.Element;
@@ -0,0 +1,275 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
4
+ import { NBIAPI } from '../api';
5
+ import { FormDialog } from './form-dialog';
6
+ import { configToInput, parseKVLines, parseMcpJsonEntry } from './claude-mcp-paste';
7
+ const SCOPES = ['user', 'project', 'local'];
8
+ const TRANSPORTS = ['stdio', 'sse', 'http'];
9
+ const SCOPE_HINT = {
10
+ user: 'available in all your projects',
11
+ project: 'shared via the project repo (.mcp.json)',
12
+ local: 'this project, this user only'
13
+ };
14
+ export function SettingsPanelComponentClaudeMCP(_props) {
15
+ var _a;
16
+ const [servers, setServers] = useState([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [error, setError] = useState(null);
19
+ const [addOpen, setAddOpen] = useState(false);
20
+ const [pendingRemoval, setPendingRemoval] = useState(null);
21
+ const [pendingToggle, setPendingToggle] = useState(null);
22
+ const refresh = async () => {
23
+ var _a;
24
+ setLoading(true);
25
+ setError(null);
26
+ try {
27
+ const list = await NBIAPI.listClaudeMCPServers();
28
+ setServers(list);
29
+ }
30
+ catch (e) {
31
+ setError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
32
+ }
33
+ finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+ useEffect(() => {
38
+ refresh();
39
+ }, []);
40
+ const handleRemove = async (srv) => {
41
+ var _a;
42
+ const ok = await showDialog({
43
+ title: 'Remove MCP server?',
44
+ body: `"${srv.name}" will be removed from Claude's ${srv.scope}-scope config.`,
45
+ buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'Remove' })]
46
+ });
47
+ if (!ok.button.accept) {
48
+ return;
49
+ }
50
+ setPendingRemoval(srv);
51
+ try {
52
+ await NBIAPI.removeClaudeMCPServer(srv.name, srv.scope);
53
+ await refresh();
54
+ }
55
+ catch (e) {
56
+ setError(`Failed to remove: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
57
+ }
58
+ finally {
59
+ setPendingRemoval(null);
60
+ }
61
+ };
62
+ const handleToggleDisabled = async (srv) => {
63
+ var _a;
64
+ setPendingToggle(srv);
65
+ try {
66
+ await NBIAPI.setClaudeMCPServerDisabled(srv.name, srv.scope, !srv.disabledForWorkspace);
67
+ await refresh();
68
+ }
69
+ catch (e) {
70
+ setError(`Failed to update workspace state: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
71
+ }
72
+ finally {
73
+ setPendingToggle(null);
74
+ }
75
+ };
76
+ const handleAddSubmit = async (input) => {
77
+ // Errors are rendered inside the dialog so they're not hidden behind the
78
+ // modal backdrop; rethrow so the dialog can keep itself open.
79
+ await NBIAPI.addClaudeMCPServer(input);
80
+ setAddOpen(false);
81
+ await refresh();
82
+ };
83
+ const grouped = {
84
+ user: [],
85
+ project: [],
86
+ local: []
87
+ };
88
+ for (const srv of servers) {
89
+ ((_a = grouped[srv.scope]) !== null && _a !== void 0 ? _a : grouped.user).push(srv);
90
+ }
91
+ return (React.createElement("div", { className: "config-dialog-body nbi-skills-panel" },
92
+ React.createElement("div", { className: "nbi-skills-header" },
93
+ React.createElement("div", { className: "nbi-skills-title" }, "Claude MCP"),
94
+ React.createElement("div", { className: "nbi-skills-header-actions" },
95
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: refresh, disabled: loading, title: "Re-read Claude's MCP config from disk" },
96
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, loading ? 'Refreshing…' : 'Refresh')),
97
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => setAddOpen(true) },
98
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Add server")))),
99
+ React.createElement("div", { className: "nbi-info-banner", role: "note" },
100
+ "These are Claude Code's own MCP servers (read from",
101
+ ' ',
102
+ React.createElement("code", null, "~/.claude.json"),
103
+ ", project ",
104
+ React.createElement("code", null, ".mcp.json"),
105
+ ", and the per-user-per-project block). Independent of the NBI MCP Servers tab."),
106
+ error && (React.createElement("div", { className: "nbi-skills-error", role: "alert" }, error)),
107
+ SCOPES.map(scope => (React.createElement(ClaudeMCPScopeSection, { key: scope, scope: scope, servers: grouped[scope], loading: loading, pendingRemoval: pendingRemoval, pendingToggle: pendingToggle, onRemove: handleRemove, onToggleDisabled: handleToggleDisabled }))),
108
+ addOpen && (React.createElement(ClaudeMCPAddDialog, { onCancel: () => setAddOpen(false), onSubmit: handleAddSubmit }))));
109
+ }
110
+ function ClaudeMCPScopeSection(props) {
111
+ return (React.createElement("div", { className: "nbi-skills-section" },
112
+ React.createElement("div", { className: "nbi-skills-section-caption", title: SCOPE_HINT[props.scope] }, props.scope.toUpperCase()),
113
+ props.servers.length === 0 ? (React.createElement("div", { className: "nbi-skills-empty" }, props.loading ? 'Loading…' : 'No servers in this scope.')) : (props.servers.map(srv => {
114
+ var _a, _b, _c, _d;
115
+ return (React.createElement(ClaudeMCPRow, { key: `${srv.scope}-${srv.name}`, srv: srv, removing: ((_a = props.pendingRemoval) === null || _a === void 0 ? void 0 : _a.name) === srv.name &&
116
+ ((_b = props.pendingRemoval) === null || _b === void 0 ? void 0 : _b.scope) === srv.scope, toggling: ((_c = props.pendingToggle) === null || _c === void 0 ? void 0 : _c.name) === srv.name &&
117
+ ((_d = props.pendingToggle) === null || _d === void 0 ? void 0 : _d.scope) === srv.scope, onRemove: () => props.onRemove(srv), onToggleDisabled: () => props.onToggleDisabled(srv) }));
118
+ }))));
119
+ }
120
+ function ClaudeMCPRow(props) {
121
+ const { srv } = props;
122
+ const summary = srv.transport === 'stdio'
123
+ ? [srv.command, ...srv.args].filter(Boolean).join(' ')
124
+ : srv.url;
125
+ const disabled = srv.disabledForWorkspace;
126
+ const toggleLabel = disabled
127
+ ? props.toggling
128
+ ? 'Enabling…'
129
+ : 'Enable for workspace'
130
+ : props.toggling
131
+ ? 'Disabling…'
132
+ : 'Disable for workspace';
133
+ const toggleTitle = disabled
134
+ ? 'Re-enable this server for the current Jupyter workspace'
135
+ : 'Hide this server from Claude in the current Jupyter workspace (other workspaces unaffected)';
136
+ return (React.createElement("div", { className: `nbi-skill-row${disabled ? ' nbi-skill-row-disabled' : ''}` },
137
+ React.createElement("div", { className: "nbi-skill-row-main" },
138
+ React.createElement("div", { className: "nbi-skill-row-name" },
139
+ srv.name,
140
+ disabled && (React.createElement("span", { className: "nbi-skill-row-badge" }, "Disabled for workspace"))),
141
+ React.createElement("div", { className: "nbi-skill-row-description" },
142
+ React.createElement("code", null, srv.transport),
143
+ summary && React.createElement("span", null,
144
+ ": ",
145
+ summary))),
146
+ React.createElement("div", { className: "nbi-skill-row-actions", onClick: e => e.stopPropagation() },
147
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: props.onToggleDisabled, disabled: props.toggling, title: toggleTitle },
148
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, toggleLabel)),
149
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: props.onRemove, disabled: props.removing },
150
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, props.removing ? 'Removing…' : 'Remove')))));
151
+ }
152
+ const JSON_PLACEHOLDER = `"server-key": {
153
+ "command": "uvx",
154
+ "args": ["server-package@latest"]
155
+ }`;
156
+ const FORM_TAB_ID = 'nbi-mcp-add-tab-form';
157
+ const JSON_TAB_ID = 'nbi-mcp-add-tab-json';
158
+ const FORM_PANEL_ID = 'nbi-mcp-add-panel-form';
159
+ const JSON_PANEL_ID = 'nbi-mcp-add-panel-json';
160
+ function ClaudeMCPAddDialog(props) {
161
+ const [scope, setScope] = useState('user');
162
+ const [mode, setMode] = useState('form');
163
+ const [transport, setTransport] = useState('stdio');
164
+ const [name, setName] = useState('');
165
+ const [commandOrUrl, setCommandOrUrl] = useState('');
166
+ const [argsText, setArgsText] = useState('');
167
+ const [envText, setEnvText] = useState('');
168
+ const [headersText, setHeadersText] = useState('');
169
+ const [jsonText, setJsonText] = useState('');
170
+ const [submitting, setSubmitting] = useState(false);
171
+ const [submitError, setSubmitError] = useState(null);
172
+ const nameInputRef = useRef(null);
173
+ const jsonInputRef = useRef(null);
174
+ const formTabRef = useRef(null);
175
+ const jsonTabRef = useRef(null);
176
+ // Skip the first effect run so we don't steal focus from the Scope select
177
+ // when the dialog mounts; only re-focus when the user actively switches.
178
+ const hasMountedRef = useRef(false);
179
+ useEffect(() => {
180
+ var _a, _b;
181
+ if (!hasMountedRef.current) {
182
+ hasMountedRef.current = true;
183
+ return;
184
+ }
185
+ if (mode === 'json') {
186
+ (_a = jsonInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
187
+ }
188
+ else {
189
+ (_b = nameInputRef.current) === null || _b === void 0 ? void 0 : _b.focus();
190
+ }
191
+ }, [mode]);
192
+ const canSubmit = mode === 'json'
193
+ ? jsonText.trim().length > 0 && !submitting
194
+ : name.trim() && commandOrUrl.trim() && !submitting;
195
+ const handleSubmit = async () => {
196
+ var _a;
197
+ if (!canSubmit) {
198
+ return;
199
+ }
200
+ setSubmitError(null);
201
+ setSubmitting(true);
202
+ try {
203
+ let input;
204
+ if (mode === 'json') {
205
+ const { name: parsedName, config } = parseMcpJsonEntry(jsonText);
206
+ input = configToInput(parsedName, config, scope);
207
+ }
208
+ else {
209
+ const argsList = argsText
210
+ .split('\n')
211
+ .map(line => line.trim())
212
+ .filter(Boolean);
213
+ input = {
214
+ name: name.trim(),
215
+ scope,
216
+ transport,
217
+ commandOrUrl: commandOrUrl.trim(),
218
+ args: transport === 'stdio' ? argsList : undefined,
219
+ env: transport === 'stdio' ? parseKVLines(envText, '=') : undefined,
220
+ headers: transport === 'stdio' ? undefined : parseKVLines(headersText, ':')
221
+ };
222
+ }
223
+ await props.onSubmit(input);
224
+ }
225
+ catch (e) {
226
+ setSubmitError((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e));
227
+ }
228
+ finally {
229
+ setSubmitting(false);
230
+ }
231
+ };
232
+ const onTabsKeyDown = (event) => {
233
+ var _a;
234
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
235
+ return;
236
+ }
237
+ event.preventDefault();
238
+ const next = mode === 'form' ? 'json' : 'form';
239
+ setMode(next);
240
+ (_a = (next === 'form' ? formTabRef : jsonTabRef).current) === null || _a === void 0 ? void 0 : _a.focus();
241
+ };
242
+ return (React.createElement(FormDialog, { title: "Add MCP server", submitLabel: "Add", submitInProgressLabel: "Adding\u2026", canSubmit: Boolean(canSubmit), submitting: submitting, error: submitError, onCancel: props.onCancel, onSubmit: handleSubmit },
243
+ React.createElement("div", { className: "nbi-form-field" },
244
+ React.createElement("label", { htmlFor: "nbi-mcp-add-scope" }, "Scope"),
245
+ React.createElement("select", { id: "nbi-mcp-add-scope", value: scope, onChange: e => setScope(e.target.value) }, SCOPES.map(s => (React.createElement("option", { key: s, value: s },
246
+ s,
247
+ " \u2014 ",
248
+ SCOPE_HINT[s]))))),
249
+ React.createElement("div", { className: "nbi-form-field" },
250
+ React.createElement("label", { id: "nbi-mcp-add-input-mode-label" }, "Input mode"),
251
+ React.createElement("div", { className: "nbi-segmented-control", role: "tablist", "aria-labelledby": "nbi-mcp-add-input-mode-label", onKeyDown: onTabsKeyDown },
252
+ React.createElement("button", { type: "button", role: "tab", id: FORM_TAB_ID, ref: formTabRef, "aria-selected": mode === 'form', "aria-controls": FORM_PANEL_ID, tabIndex: mode === 'form' ? 0 : -1, className: `nbi-segmented-control-option${mode === 'form' ? ' is-active' : ''}`, onClick: () => setMode('form') }, "Form"),
253
+ React.createElement("button", { type: "button", role: "tab", id: JSON_TAB_ID, ref: jsonTabRef, "aria-selected": mode === 'json', "aria-controls": JSON_PANEL_ID, tabIndex: mode === 'json' ? 0 : -1, className: `nbi-segmented-control-option${mode === 'json' ? ' is-active' : ''}`, onClick: () => setMode('json') }, "JSON"))),
254
+ mode === 'json' ? (React.createElement("div", { className: "nbi-form-field", role: "tabpanel", id: JSON_PANEL_ID, "aria-labelledby": JSON_TAB_ID },
255
+ React.createElement("label", { htmlFor: "nbi-mcp-add-json" }, "JSON"),
256
+ React.createElement("textarea", { id: "nbi-mcp-add-json", ref: jsonInputRef, rows: 10, value: jsonText, onChange: e => setJsonText(e.target.value), placeholder: JSON_PLACEHOLDER, spellCheck: false, autoFocus: true }))) : (React.createElement("div", { role: "tabpanel", id: FORM_PANEL_ID, "aria-labelledby": FORM_TAB_ID, className: "nbi-mcp-add-form-panel" },
257
+ React.createElement("div", { className: "nbi-form-field" },
258
+ React.createElement("label", { htmlFor: "nbi-mcp-add-name" }, "Name"),
259
+ React.createElement("input", { id: "nbi-mcp-add-name", ref: nameInputRef, type: "text", value: name, onChange: e => setName(e.target.value), placeholder: "my-server", autoFocus: true })),
260
+ React.createElement("div", { className: "nbi-form-field" },
261
+ React.createElement("label", { htmlFor: "nbi-mcp-add-transport" }, "Transport"),
262
+ React.createElement("select", { id: "nbi-mcp-add-transport", value: transport, onChange: e => setTransport(e.target.value) }, TRANSPORTS.map(t => (React.createElement("option", { key: t, value: t }, t))))),
263
+ React.createElement("div", { className: "nbi-form-field" },
264
+ React.createElement("label", { htmlFor: "nbi-mcp-add-command-or-url" }, transport === 'stdio' ? 'Command' : 'URL'),
265
+ React.createElement("input", { id: "nbi-mcp-add-command-or-url", type: "text", value: commandOrUrl, onChange: e => setCommandOrUrl(e.target.value), placeholder: transport === 'stdio' ? 'npx' : 'https://example.com/mcp' })),
266
+ transport === 'stdio' && (React.createElement("div", { className: "nbi-form-field" },
267
+ React.createElement("label", { htmlFor: "nbi-mcp-add-args" }, "Args (one per line)"),
268
+ React.createElement("textarea", { id: "nbi-mcp-add-args", rows: 3, value: argsText, onChange: e => setArgsText(e.target.value), placeholder: '-y\n@scope/package@latest' }))),
269
+ transport === 'stdio' && (React.createElement("div", { className: "nbi-form-field" },
270
+ React.createElement("label", { htmlFor: "nbi-mcp-add-env" }, "Environment (KEY=value, one per line)"),
271
+ React.createElement("textarea", { id: "nbi-mcp-add-env", rows: 3, value: envText, onChange: e => setEnvText(e.target.value), placeholder: "API_KEY=\u2026" }))),
272
+ transport !== 'stdio' && (React.createElement("div", { className: "nbi-form-field" },
273
+ React.createElement("label", { htmlFor: "nbi-mcp-add-headers" }, "Headers (Name: value, one per line)"),
274
+ React.createElement("textarea", { id: "nbi-mcp-add-headers", rows: 3, value: headersText, onChange: e => setHeadersText(e.target.value), placeholder: "Authorization: Bearer \u2026" })))))));
275
+ }
@@ -0,0 +1,7 @@
1
+ import { ClaudeMCPScope, IClaudeMCPAddInput } from '../api';
2
+ export declare function parseMcpJsonEntry(raw: string): {
3
+ name: string;
4
+ config: Record<string, any>;
5
+ };
6
+ export declare function configToInput(name: string, config: Record<string, any>, scope: ClaudeMCPScope): IClaudeMCPAddInput;
7
+ export declare function parseKVLines(text: string, separator: string): Record<string, string>;
@@ -0,0 +1,104 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+ const VALID_TRANSPORTS = [
3
+ 'stdio',
4
+ 'sse',
5
+ 'http'
6
+ ];
7
+ // Accept any of these paste shapes:
8
+ // "server-key": { ... } (bare key + object)
9
+ // { "server-key": { ... } } (wrapped, one entry)
10
+ // { "mcpServers": { "server-key": { ... } } } (full mcp.json)
11
+ export function parseMcpJsonEntry(raw) {
12
+ const trimmed = raw.trim().replace(/^,|,$/g, '');
13
+ if (!trimmed) {
14
+ throw new Error('JSON is empty.');
15
+ }
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(trimmed);
19
+ }
20
+ catch (_a) {
21
+ try {
22
+ parsed = JSON.parse(`{${trimmed}}`);
23
+ }
24
+ catch (e) {
25
+ throw new Error(`Invalid JSON: ${e.message}`);
26
+ }
27
+ }
28
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
29
+ throw new Error('JSON must describe a server entry.');
30
+ }
31
+ if (parsed.mcpServers && typeof parsed.mcpServers === 'object') {
32
+ parsed = parsed.mcpServers;
33
+ }
34
+ const keys = Object.keys(parsed);
35
+ if (keys.length === 0) {
36
+ throw new Error('Expected at least one server entry.');
37
+ }
38
+ if (keys.length > 1) {
39
+ throw new Error('Multiple servers found; paste one entry at a time.');
40
+ }
41
+ const name = keys[0];
42
+ const config = parsed[name];
43
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
44
+ throw new Error(`Server "${name}" config must be an object.`);
45
+ }
46
+ return { name, config };
47
+ }
48
+ export function configToInput(name, config, scope) {
49
+ const url = typeof config.url === 'string' ? config.url.trim() : '';
50
+ const command = typeof config.command === 'string' ? config.command.trim() : '';
51
+ if (!url && !command) {
52
+ throw new Error('Server config must include "command" or "url".');
53
+ }
54
+ const explicitTransport = typeof config.transport === 'string' &&
55
+ VALID_TRANSPORTS.includes(config.transport)
56
+ ? config.transport
57
+ : null;
58
+ // If the user pasted an explicit `transport`, it must match the shape of
59
+ // the rest of the config: `stdio` requires `command`, `sse`/`http` require
60
+ // `url`. A mismatch is more likely a typo than intent, and silently
61
+ // downgrading to whatever shape we see is the footgun reviewers flagged.
62
+ if (explicitTransport === 'stdio' && !command) {
63
+ throw new Error('transport: "stdio" requires a "command" field.');
64
+ }
65
+ if ((explicitTransport === 'http' || explicitTransport === 'sse') && !url) {
66
+ throw new Error(`transport: "${explicitTransport}" requires a "url" field.`);
67
+ }
68
+ const transport = explicitTransport !== null && explicitTransport !== void 0 ? explicitTransport : (url ? 'http' : 'stdio');
69
+ const commandOrUrl = transport === 'stdio' ? command : url;
70
+ return {
71
+ name,
72
+ scope,
73
+ transport,
74
+ commandOrUrl,
75
+ args: transport === 'stdio' && Array.isArray(config.args)
76
+ ? config.args.map(String)
77
+ : undefined,
78
+ env: transport === 'stdio' ? toStringRecord(config.env) : undefined,
79
+ headers: transport !== 'stdio' ? toStringRecord(config.headers) : undefined
80
+ };
81
+ }
82
+ export function parseKVLines(text, separator) {
83
+ const out = {};
84
+ for (const line of text.split('\n')) {
85
+ const trimmed = line.trim();
86
+ if (!trimmed) {
87
+ continue;
88
+ }
89
+ const idx = trimmed.indexOf(separator);
90
+ if (idx > 0) {
91
+ out[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ function toStringRecord(obj) {
97
+ const out = {};
98
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
99
+ for (const [k, v] of Object.entries(obj)) {
100
+ out[String(k)] = String(v);
101
+ }
102
+ }
103
+ return out;
104
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="react" />
2
+ import { IClaudeSessionInfo, IClaudeSessionList } from '../api';
3
+ export interface IClaudeSessionPickerProps {
4
+ onResume: (session: IClaudeSessionInfo) => void;
5
+ onClose: () => void;
6
+ fetchSessions?: () => Promise<IClaudeSessionList>;
7
+ }
8
+ export declare function ClaudeSessionPicker(props: IClaudeSessionPickerProps): JSX.Element;