@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,214 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+
3
+ import React, {
4
+ KeyboardEvent,
5
+ MouseEvent,
6
+ useEffect,
7
+ useRef,
8
+ useState
9
+ } from 'react';
10
+ import { VscCheck, VscClose, VscCopy, VscHistory } from '../icons';
11
+
12
+ import { IClaudeSessionInfo, IClaudeSessionList, NBIAPI } from '../api';
13
+ import { buildResumeCommand, writeTextToClipboard } from '../utils';
14
+
15
+ export interface IClaudeSessionPickerProps {
16
+ onResume: (session: IClaudeSessionInfo) => void;
17
+ onClose: () => void;
18
+ fetchSessions?: () => Promise<IClaudeSessionList>;
19
+ }
20
+
21
+ function formatTimestamp(epochSeconds: number): string {
22
+ if (!epochSeconds) {
23
+ return '';
24
+ }
25
+ const date = new Date(epochSeconds * 1000);
26
+ if (Number.isNaN(date.getTime())) {
27
+ return '';
28
+ }
29
+ return date.toLocaleString();
30
+ }
31
+
32
+ const COPY_LABELS: Record<'copied' | 'failed' | 'idle', string> = {
33
+ idle: 'Copy resume command',
34
+ copied: 'Resume command copied',
35
+ failed: 'Failed to copy resume command'
36
+ };
37
+
38
+ export function ClaudeSessionPicker(
39
+ props: IClaudeSessionPickerProps
40
+ ): JSX.Element {
41
+ const [sessions, setSessions] = useState<IClaudeSessionInfo[]>([]);
42
+ const [currentCwd, setCurrentCwd] = useState('');
43
+ const [loading, setLoading] = useState(true);
44
+ const [resuming, setResuming] = useState(false);
45
+ const [error, setError] = useState('');
46
+ const [copyFeedback, setCopyFeedback] = useState<{
47
+ sessionId: string;
48
+ status: 'copied' | 'failed';
49
+ } | null>(null);
50
+ const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
51
+
52
+ useEffect(() => {
53
+ return () => {
54
+ if (copyTimerRef.current !== null) {
55
+ clearTimeout(copyTimerRef.current);
56
+ }
57
+ };
58
+ }, []);
59
+
60
+ useEffect(() => {
61
+ let cancelled = false;
62
+ const fetch =
63
+ props.fetchSessions ?? (() => NBIAPI.listClaudeSessions('cwd'));
64
+ fetch()
65
+ .then(result => {
66
+ if (cancelled) {
67
+ return;
68
+ }
69
+ setSessions(result.sessions);
70
+ setCurrentCwd(result.currentCwd);
71
+ setLoading(false);
72
+ })
73
+ .catch(reason => {
74
+ if (cancelled) {
75
+ return;
76
+ }
77
+ setError(String(reason?.message ?? reason ?? 'Unknown error'));
78
+ setLoading(false);
79
+ });
80
+ return () => {
81
+ cancelled = true;
82
+ };
83
+ }, []);
84
+
85
+ const handleCopyResumeCommand = async (
86
+ event: MouseEvent<HTMLButtonElement>,
87
+ session: IClaudeSessionInfo
88
+ ) => {
89
+ event.stopPropagation();
90
+ event.preventDefault();
91
+ const ok = await writeTextToClipboard(
92
+ buildResumeCommand(currentCwd, session.session_id)
93
+ );
94
+ setCopyFeedback({
95
+ sessionId: session.session_id,
96
+ status: ok ? 'copied' : 'failed'
97
+ });
98
+ if (copyTimerRef.current !== null) {
99
+ clearTimeout(copyTimerRef.current);
100
+ }
101
+ copyTimerRef.current = setTimeout(() => {
102
+ setCopyFeedback(null);
103
+ copyTimerRef.current = null;
104
+ }, 1500);
105
+ };
106
+
107
+ const handleResume = async (session: IClaudeSessionInfo) => {
108
+ if (resuming) {
109
+ return;
110
+ }
111
+ setResuming(true);
112
+ // When a custom fetchSessions is provided the caller owns the resume
113
+ // lifecycle (e.g. the launcher tile opens a terminal directly), so skip
114
+ // the NBI sidebar API call which requires Claude Code mode to be active.
115
+ if (props.fetchSessions) {
116
+ props.onResume(session);
117
+ return;
118
+ }
119
+ try {
120
+ await NBIAPI.resumeClaudeSession(session.session_id);
121
+ props.onResume(session);
122
+ } catch (reason) {
123
+ setError(String((reason as Error)?.message ?? reason ?? 'Unknown error'));
124
+ setResuming(false);
125
+ }
126
+ };
127
+
128
+ return (
129
+ <div
130
+ className="workspace-file-popover claude-session-picker"
131
+ tabIndex={1}
132
+ autoFocus={true}
133
+ onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {
134
+ if (event.key === 'Escape') {
135
+ event.stopPropagation();
136
+ event.preventDefault();
137
+ props.onClose();
138
+ }
139
+ }}
140
+ >
141
+ <div className="mode-tools-popover-header">
142
+ <div className="mode-tools-popover-header-icon">
143
+ <VscHistory />
144
+ </div>
145
+ <div className="mode-tools-popover-title">Resume Claude session</div>
146
+ <div style={{ flexGrow: 1 }}></div>
147
+ <div
148
+ className="mode-tools-popover-button mode-tools-popover-close-button"
149
+ title="Close"
150
+ onClick={props.onClose}
151
+ >
152
+ <VscClose />
153
+ </div>
154
+ </div>
155
+ <div className="workspace-file-popover-body">
156
+ {error && (
157
+ <div className="workspace-file-popover-status error">{error}</div>
158
+ )}
159
+ {loading ? (
160
+ <div className="workspace-file-popover-status">
161
+ Loading sessions&#8230;
162
+ </div>
163
+ ) : sessions.length === 0 ? (
164
+ <div className="workspace-file-popover-status">
165
+ No previous Claude sessions found for this working directory.
166
+ </div>
167
+ ) : (
168
+ <ul className="claude-session-picker-list">
169
+ {sessions.map(session => {
170
+ const feedback =
171
+ copyFeedback && copyFeedback.sessionId === session.session_id
172
+ ? copyFeedback.status
173
+ : null;
174
+ const buttonLabel = COPY_LABELS[feedback ?? 'idle'];
175
+ return (
176
+ <li
177
+ key={session.session_id}
178
+ className={`claude-session-picker-item${resuming ? ' busy' : ''}`}
179
+ onClick={() => handleResume(session)}
180
+ >
181
+ {session.preview && (
182
+ <div className="claude-session-picker-item-preview">
183
+ {session.preview}
184
+ </div>
185
+ )}
186
+ <div className="claude-session-picker-item-meta">
187
+ <span>{formatTimestamp(session.modified_at)}</span>
188
+ <span
189
+ className="claude-session-picker-item-id"
190
+ title={session.session_id}
191
+ >
192
+ {session.session_id.slice(0, 8)}
193
+ </span>
194
+ <button
195
+ type="button"
196
+ className={`claude-session-picker-item-copy${
197
+ feedback ? ` ${feedback}` : ''
198
+ }`}
199
+ title={buttonLabel}
200
+ aria-label={buttonLabel}
201
+ onClick={event => handleCopyResumeCommand(event, session)}
202
+ >
203
+ {feedback === 'copied' ? <VscCheck /> : <VscCopy />}
204
+ </button>
205
+ </div>
206
+ </li>
207
+ );
208
+ })}
209
+ </ul>
210
+ )}
211
+ </div>
212
+ </div>
213
+ );
214
+ }
@@ -0,0 +1,75 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+
3
+ import React, { ReactNode } from 'react';
4
+
5
+ /**
6
+ * Modal-form shell shared by the simple add/install dialogs across the
7
+ * Claude-MCP and Plugins panels. Owns:
8
+ * * backdrop + card layout (cancel-on-backdrop-click)
9
+ * * title / body / actions slots
10
+ * * Escape-to-cancel keyboard handler
11
+ * * inline error rendering when ``error`` is non-null
12
+ * * primary button label transitions for the submitting state
13
+ *
14
+ * Multi-step flows (e.g. the GitHub-import preview-then-install dialog in
15
+ * ``skills-panel.tsx``) keep their own shell since fitting them here
16
+ * would bloat the abstraction.
17
+ */
18
+ export function FormDialog(props: {
19
+ title: string;
20
+ submitLabel: string;
21
+ submitInProgressLabel?: string;
22
+ canSubmit: boolean;
23
+ submitting: boolean;
24
+ error?: string | null;
25
+ onCancel: () => void;
26
+ onSubmit: () => void;
27
+ children: ReactNode;
28
+ }): JSX.Element {
29
+ return (
30
+ <div className="nbi-modal-backdrop" onClick={props.onCancel}>
31
+ <div
32
+ className="nbi-modal-card"
33
+ role="dialog"
34
+ aria-modal="true"
35
+ onClick={e => e.stopPropagation()}
36
+ onKeyDown={e => {
37
+ if (e.key === 'Escape' && !props.submitting) {
38
+ props.onCancel();
39
+ }
40
+ }}
41
+ tabIndex={-1}
42
+ >
43
+ <div className="nbi-modal-title">{props.title}</div>
44
+ <div className="nbi-modal-body">
45
+ {props.children}
46
+ {props.error && (
47
+ <div className="nbi-skills-error" role="alert">
48
+ {props.error}
49
+ </div>
50
+ )}
51
+ </div>
52
+ <div className="nbi-modal-actions">
53
+ <button
54
+ className="jp-Dialog-button jp-mod-reject jp-mod-styled"
55
+ onClick={props.onCancel}
56
+ disabled={props.submitting}
57
+ >
58
+ <div className="jp-Dialog-buttonLabel">Cancel</div>
59
+ </button>
60
+ <button
61
+ className="jp-Dialog-button jp-mod-accept jp-mod-styled"
62
+ onClick={props.onSubmit}
63
+ disabled={!props.canSubmit}
64
+ >
65
+ <div className="jp-Dialog-buttonLabel">
66
+ {props.submitting
67
+ ? (props.submitInProgressLabel ?? `${props.submitLabel}…`)
68
+ : props.submitLabel}
69
+ </div>
70
+ </button>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,237 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+
3
+ import React, {
4
+ ChangeEvent,
5
+ KeyboardEvent,
6
+ useEffect,
7
+ useRef,
8
+ useState
9
+ } from 'react';
10
+ import { IClaudeSessionInfo, NBIAPI } from '../api';
11
+
12
+ export interface ILauncherPickerProps {
13
+ onSessionSelected: (session: IClaudeSessionInfo) => void;
14
+ }
15
+
16
+ // Pick a short, glanceable label for a session's project. The basename
17
+ // of the cwd is usually the project's actual name; the full path stays
18
+ // available via the row's `title` attribute on hover.
19
+ function projectLabel(cwd: string | undefined | null): string {
20
+ if (!cwd) {
21
+ return '';
22
+ }
23
+ const trimmed = cwd.replace(/\/+$/, '');
24
+ const idx = trimmed.lastIndexOf('/');
25
+ return idx >= 0 ? trimmed.slice(idx + 1) : trimmed;
26
+ }
27
+
28
+ export function LauncherPicker({
29
+ onSessionSelected
30
+ }: ILauncherPickerProps): JSX.Element {
31
+ const [sessions, setSessions] = useState<IClaudeSessionInfo[]>([]);
32
+ const [loading, setLoading] = useState(true);
33
+ const [error, setError] = useState('');
34
+ const [filter, setFilter] = useState('');
35
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
36
+ const containerRef = useRef<HTMLDivElement>(null);
37
+ const listRef = useRef<HTMLDivElement>(null);
38
+
39
+ useEffect(() => {
40
+ NBIAPI.listClaudeSessions('all')
41
+ .then(result => {
42
+ setSessions(result.sessions);
43
+ setLoading(false);
44
+ })
45
+ .catch((reason: any) => {
46
+ setError(String(reason?.message ?? reason ?? 'Unknown error'));
47
+ setLoading(false);
48
+ });
49
+ }, []);
50
+
51
+ const needle = filter.toLowerCase();
52
+ const filtered = filter
53
+ ? sessions.filter(
54
+ s =>
55
+ s.preview?.toLowerCase().includes(needle) ||
56
+ s.cwd?.toLowerCase().includes(needle)
57
+ )
58
+ : sessions;
59
+
60
+ // A held-over index against a refetched session set could silently
61
+ // point at a different session, so reset on any sessions change —
62
+ // not just length, which would miss equal-length-but-different sets.
63
+ useEffect(() => {
64
+ setHighlightedIndex(-1);
65
+ }, [filter, sessions]);
66
+
67
+ useEffect(() => {
68
+ if (highlightedIndex < 0 || !listRef.current) {
69
+ return;
70
+ }
71
+ const row = listRef.current.children[highlightedIndex] as
72
+ | HTMLElement
73
+ | undefined;
74
+ row?.scrollIntoView({ block: 'nearest' });
75
+ }, [highlightedIndex]);
76
+
77
+ // Refs so the document-level keydown listener installed below can read
78
+ // the latest values without re-attaching on every render.
79
+ const filteredRef = useRef(filtered);
80
+ const highlightedIndexRef = useRef(highlightedIndex);
81
+ const onSessionSelectedRef = useRef(onSessionSelected);
82
+ filteredRef.current = filtered;
83
+ highlightedIndexRef.current = highlightedIndex;
84
+ onSessionSelectedRef.current = onSessionSelected;
85
+
86
+ // Lumino's Dialog catches Enter at capture phase on the dialog node and
87
+ // triggers its default OK button (the "New Session" button), so a React
88
+ // bubble-phase handler never sees Enter inside the picker. Attach a
89
+ // document-capture listener here so we beat the dialog and activate the
90
+ // highlighted row instead — falling through to the dialog's New Session
91
+ // path only when there's nothing selectable to land on.
92
+ useEffect(() => {
93
+ const handler = (e: globalThis.KeyboardEvent) => {
94
+ if (e.key !== 'Enter') {
95
+ return;
96
+ }
97
+ const node = containerRef.current;
98
+ if (!node || !node.contains(e.target as Node)) {
99
+ return;
100
+ }
101
+ const list = filteredRef.current;
102
+ if (list.length === 0) {
103
+ return;
104
+ }
105
+ e.preventDefault();
106
+ e.stopImmediatePropagation();
107
+ const idx = highlightedIndexRef.current;
108
+ onSessionSelectedRef.current(list[idx >= 0 ? idx : 0]);
109
+ };
110
+ document.addEventListener('keydown', handler, { capture: true });
111
+ return () =>
112
+ document.removeEventListener('keydown', handler, { capture: true });
113
+ }, []);
114
+
115
+ const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
116
+ if (filtered.length === 0) {
117
+ return;
118
+ }
119
+ // From "no row highlighted" (-1), ArrowDown jumps to the first row
120
+ // and ArrowUp jumps to the last — each direction lands at its
121
+ // nearest end so the user always reaches a valid row in one press.
122
+ if (e.key === 'ArrowDown') {
123
+ e.preventDefault();
124
+ setHighlightedIndex(i => (i < 0 || i >= filtered.length - 1 ? 0 : i + 1));
125
+ } else if (e.key === 'ArrowUp') {
126
+ e.preventDefault();
127
+ setHighlightedIndex(i => (i <= 0 ? filtered.length - 1 : i - 1));
128
+ }
129
+ };
130
+
131
+ if (loading) {
132
+ return (
133
+ <div className="nbi-claude-code-picker-status">
134
+ Loading sessions&#8230;
135
+ </div>
136
+ );
137
+ }
138
+ if (error) {
139
+ return (
140
+ <div className="nbi-claude-code-picker-status nbi-claude-code-picker-error">
141
+ {error}
142
+ </div>
143
+ );
144
+ }
145
+ const activeRowId =
146
+ highlightedIndex >= 0
147
+ ? `nbi-claude-session-row-${filtered[highlightedIndex].session_id}`
148
+ : undefined;
149
+ return (
150
+ <div
151
+ className="nbi-claude-code-picker-body"
152
+ ref={containerRef}
153
+ onKeyDown={handleKeyDown}
154
+ >
155
+ <input
156
+ className="nbi-claude-code-picker-search"
157
+ type="text"
158
+ placeholder="Filter sessions..."
159
+ value={filter}
160
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
161
+ setFilter(e.target.value)
162
+ }
163
+ autoFocus
164
+ role="combobox"
165
+ aria-expanded={filtered.length > 0}
166
+ aria-controls="nbi-claude-session-listbox"
167
+ aria-activedescendant={activeRowId}
168
+ />
169
+ <div
170
+ className="nbi-claude-code-picker-list"
171
+ id="nbi-claude-session-listbox"
172
+ role="listbox"
173
+ ref={listRef}
174
+ >
175
+ {filtered.length === 0 ? (
176
+ <div className="nbi-claude-code-picker-empty">
177
+ {filter
178
+ ? 'No sessions match your filter.'
179
+ : 'No previous sessions found.'}
180
+ </div>
181
+ ) : (
182
+ filtered.map((session, index) => {
183
+ const isHighlighted = index === highlightedIndex;
184
+ return (
185
+ <div
186
+ key={session.session_id}
187
+ id={`nbi-claude-session-row-${session.session_id}`}
188
+ role="option"
189
+ className={
190
+ 'nbi-claude-code-picker-session' +
191
+ (isHighlighted ? ' highlighted' : '')
192
+ }
193
+ tabIndex={0}
194
+ aria-selected={isHighlighted}
195
+ onClick={() => onSessionSelected(session)}
196
+ onFocus={() => setHighlightedIndex(index)}
197
+ >
198
+ <div className="nbi-claude-code-picker-session-top">
199
+ <span className="nbi-claude-code-picker-session-id">
200
+ {session.session_id.slice(0, 8)}
201
+ </span>
202
+ {/* Render the basename of the project as the inline
203
+ label; keep the full path on hover via title so a
204
+ user with several similarly-named projects can
205
+ disambiguate without losing the path entirely.
206
+ Screen readers don't expose `title` on <span>
207
+ reliably, so duplicate the full path into
208
+ aria-label whenever it differs from the visible
209
+ basename. */}
210
+ {(() => {
211
+ const full = session.cwd ?? '';
212
+ const label = projectLabel(full);
213
+ const fullDiffersFromLabel = full && full !== label;
214
+ return (
215
+ <span
216
+ className="nbi-claude-code-picker-session-project"
217
+ title={full}
218
+ aria-label={fullDiffersFromLabel ? full : undefined}
219
+ >
220
+ {label}
221
+ </span>
222
+ );
223
+ })()}
224
+ </div>
225
+ {session.preview && (
226
+ <div className="nbi-claude-code-picker-msg">
227
+ {session.preview}
228
+ </div>
229
+ )}
230
+ </div>
231
+ );
232
+ })
233
+ )}
234
+ </div>
235
+ </div>
236
+ );
237
+ }
@@ -0,0 +1,53 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+
3
+ export function mcpServerSettingsToEnabledState(
4
+ mcpServers: any,
5
+ mcpServerSettings: any
6
+ ) {
7
+ const mcpServerEnabledState = new Map<string, Set<string>>();
8
+ for (const server of mcpServers) {
9
+ const mcpServerToolEnabledState = mcpServerSettingsToServerToolEnabledState(
10
+ mcpServers,
11
+ mcpServerSettings,
12
+ server.id
13
+ );
14
+ if (mcpServerToolEnabledState) {
15
+ mcpServerEnabledState.set(server.id, mcpServerToolEnabledState);
16
+ }
17
+ }
18
+
19
+ return mcpServerEnabledState;
20
+ }
21
+
22
+ export function mcpServerSettingsToServerToolEnabledState(
23
+ mcpServers: any,
24
+ mcpServerSettings: any,
25
+ serverId: string
26
+ ) {
27
+ const server = mcpServers.find((server: any) => server.id === serverId);
28
+
29
+ let mcpServerToolEnabledState: Set<string> | null = null;
30
+
31
+ if (!server) {
32
+ return mcpServerToolEnabledState;
33
+ }
34
+
35
+ if (mcpServerSettings[server.id]) {
36
+ const serverSettings = mcpServerSettings[server.id];
37
+ if (!serverSettings.disabled) {
38
+ mcpServerToolEnabledState = new Set<string>();
39
+ for (const tool of server.tools) {
40
+ if (!serverSettings.disabled_tools?.includes(tool.name)) {
41
+ mcpServerToolEnabledState.add(tool.name);
42
+ }
43
+ }
44
+ }
45
+ } else {
46
+ mcpServerToolEnabledState = new Set<string>();
47
+ for (const tool of server.tools) {
48
+ mcpServerToolEnabledState.add(tool.name);
49
+ }
50
+ }
51
+
52
+ return mcpServerToolEnabledState;
53
+ }
@@ -0,0 +1,127 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+
3
+ import React, {
4
+ ChangeEvent,
5
+ KeyboardEvent,
6
+ useEffect,
7
+ useRef,
8
+ useState
9
+ } from 'react';
10
+ import { VscClose, VscSend, VscSparkle } from '../icons';
11
+
12
+ import { CheckBoxItem } from './checkbox';
13
+
14
+ export interface INotebookGenerationPopoverProps {
15
+ initialShowInChat?: boolean;
16
+ onSubmit: (prompt: string, showInChat: boolean) => void;
17
+ onClose: () => void;
18
+ }
19
+
20
+ export function NotebookGenerationPopover(
21
+ props: INotebookGenerationPopoverProps
22
+ ): JSX.Element {
23
+ const [prompt, setPrompt] = useState('');
24
+ const [showInChat, setShowInChat] = useState<boolean>(
25
+ props.initialShowInChat ?? true
26
+ );
27
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
28
+
29
+ useEffect(() => {
30
+ // Defer to the next frame so the focus call lands after Lumino's
31
+ // attach lifecycle and JupyterLab's focus tracker have settled —
32
+ // otherwise the active notebook can win the focus race and the
33
+ // textarea stays unfocused (issue #231).
34
+ const handle = window.requestAnimationFrame(() => {
35
+ textareaRef.current?.focus();
36
+ });
37
+ return () => window.cancelAnimationFrame(handle);
38
+ }, []);
39
+
40
+ const trimmed = prompt.trim();
41
+ const canSubmit = trimmed.length > 0;
42
+
43
+ const handleSubmit = () => {
44
+ if (!canSubmit) {
45
+ return;
46
+ }
47
+ props.onSubmit(trimmed, showInChat);
48
+ };
49
+
50
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
51
+ if (event.key === 'Escape') {
52
+ event.stopPropagation();
53
+ event.preventDefault();
54
+ props.onClose();
55
+ return;
56
+ }
57
+ };
58
+
59
+ const handleTextareaKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
60
+ if (event.key === 'Enter' && !event.shiftKey) {
61
+ event.preventDefault();
62
+ handleSubmit();
63
+ }
64
+ };
65
+
66
+ return (
67
+ <div
68
+ className="notebook-generation-popover"
69
+ tabIndex={-1}
70
+ onKeyDown={handleKeyDown}
71
+ >
72
+ <div className="notebook-generation-popover-header">
73
+ <div className="notebook-generation-popover-header-icon">
74
+ <VscSparkle />
75
+ </div>
76
+ <div className="notebook-generation-popover-title">
77
+ Update active notebook
78
+ </div>
79
+ <div style={{ flexGrow: 1 }}></div>
80
+ <button
81
+ type="button"
82
+ className="notebook-generation-popover-close-button"
83
+ aria-label="Close notebook generation popover"
84
+ title="Close"
85
+ onClick={props.onClose}
86
+ >
87
+ <VscClose aria-hidden="true" />
88
+ </button>
89
+ </div>
90
+ <div className="notebook-generation-popover-body">
91
+ <textarea
92
+ ref={textareaRef}
93
+ className="notebook-generation-popover-input"
94
+ rows={4}
95
+ placeholder="Describe how to update the active notebook..."
96
+ value={prompt}
97
+ onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
98
+ setPrompt(event.target.value)
99
+ }
100
+ onKeyDown={handleTextareaKeyDown}
101
+ />
102
+ <CheckBoxItem
103
+ checked={showInChat}
104
+ label="Show in chat"
105
+ tooltip={
106
+ 'When enabled, the prompt opens the Notebook Intelligence ' +
107
+ 'chat sidebar. Disable to keep the chat hidden and only ' +
108
+ 'show progress on the notebook toolbar.'
109
+ }
110
+ onClick={() => setShowInChat(value => !value)}
111
+ />
112
+ <div className="notebook-generation-popover-actions">
113
+ <button
114
+ type="button"
115
+ className="notebook-generation-popover-submit"
116
+ disabled={!canSubmit}
117
+ onClick={handleSubmit}
118
+ title="Generate (Enter)"
119
+ >
120
+ <VscSend />
121
+ <span>Generate</span>
122
+ </button>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ );
127
+ }