@plmbr/notebook-intelligence 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +412 -0
- package/lib/api.d.ts +288 -0
- package/lib/api.js +927 -0
- package/lib/cell-output-bundle.d.ts +25 -0
- package/lib/cell-output-bundle.js +129 -0
- package/lib/cell-output-toolbar.d.ts +26 -0
- package/lib/cell-output-toolbar.js +188 -0
- package/lib/chat-progress-feedback.d.ts +3 -0
- package/lib/chat-progress-feedback.js +27 -0
- package/lib/chat-sidebar.d.ts +92 -0
- package/lib/chat-sidebar.js +3452 -0
- package/lib/command-ids.d.ts +39 -0
- package/lib/command-ids.js +44 -0
- package/lib/components/ask-user-question.d.ts +2 -0
- package/lib/components/ask-user-question.js +85 -0
- package/lib/components/checkbox.d.ts +2 -0
- package/lib/components/checkbox.js +30 -0
- package/lib/components/claude-mcp-panel.d.ts +2 -0
- package/lib/components/claude-mcp-panel.js +275 -0
- package/lib/components/claude-mcp-paste.d.ts +7 -0
- package/lib/components/claude-mcp-paste.js +104 -0
- package/lib/components/claude-session-picker.d.ts +8 -0
- package/lib/components/claude-session-picker.js +127 -0
- package/lib/components/form-dialog.d.ts +25 -0
- package/lib/components/form-dialog.js +35 -0
- package/lib/components/launcher-picker.d.ts +6 -0
- package/lib/components/launcher-picker.js +135 -0
- package/lib/components/mcp-util.d.ts +2 -0
- package/lib/components/mcp-util.js +37 -0
- package/lib/components/notebook-generation-popover.d.ts +7 -0
- package/lib/components/notebook-generation-popover.js +60 -0
- package/lib/components/pill.d.ts +2 -0
- package/lib/components/pill.js +5 -0
- package/lib/components/plugins-panel.d.ts +3 -0
- package/lib/components/plugins-panel.js +466 -0
- package/lib/components/settings-panel.d.ts +11 -0
- package/lib/components/settings-panel.js +742 -0
- package/lib/components/skills-panel.d.ts +2 -0
- package/lib/components/skills-panel.js +1264 -0
- package/lib/handler.d.ts +8 -0
- package/lib/handler.js +36 -0
- package/lib/icons.d.ts +45 -0
- package/lib/icons.js +54 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +2079 -0
- package/lib/markdown-renderer.d.ts +10 -0
- package/lib/markdown-renderer.js +64 -0
- package/lib/notebook-generation-toolbar.d.ts +16 -0
- package/lib/notebook-generation-toolbar.js +197 -0
- package/lib/notebook-generation.d.ts +8 -0
- package/lib/notebook-generation.js +12 -0
- package/lib/open-file-refresh-watcher-env.d.ts +4 -0
- package/lib/open-file-refresh-watcher-env.js +33 -0
- package/lib/open-file-refresh-watcher.d.ts +97 -0
- package/lib/open-file-refresh-watcher.js +190 -0
- package/lib/shell-utils.d.ts +6 -0
- package/lib/shell-utils.js +9 -0
- package/lib/task-target-notebook.d.ts +2 -0
- package/lib/task-target-notebook.js +28 -0
- package/lib/terminal-drag-format.d.ts +9 -0
- package/lib/terminal-drag-format.js +23 -0
- package/lib/terminal-drag.d.ts +12 -0
- package/lib/terminal-drag.js +268 -0
- package/lib/tokens.d.ts +149 -0
- package/lib/tokens.js +88 -0
- package/lib/tour/tour-anchors.d.ts +18 -0
- package/lib/tour/tour-anchors.js +18 -0
- package/lib/tour/tour-config.d.ts +66 -0
- package/lib/tour/tour-config.js +99 -0
- package/lib/tour/tour-defaults.json +58 -0
- package/lib/tour/tour-events.d.ts +19 -0
- package/lib/tour/tour-events.js +30 -0
- package/lib/tour/tour-overlay.d.ts +6 -0
- package/lib/tour/tour-overlay.js +350 -0
- package/lib/tour/tour-state.d.ts +20 -0
- package/lib/tour/tour-state.js +81 -0
- package/lib/tour/tour-steps.d.ts +33 -0
- package/lib/tour/tour-steps.js +216 -0
- package/lib/utils.d.ts +53 -0
- package/lib/utils.js +385 -0
- package/package.json +258 -0
- package/schema/plugin.json +42 -0
- package/src/api.ts +1424 -0
- package/src/cell-output-bundle.ts +176 -0
- package/src/cell-output-toolbar.ts +232 -0
- package/src/chat-progress-feedback.ts +35 -0
- package/src/chat-sidebar.tsx +5147 -0
- package/src/command-ids.ts +67 -0
- package/src/components/ask-user-question.tsx +151 -0
- package/src/components/checkbox.tsx +62 -0
- package/src/components/claude-mcp-panel.tsx +543 -0
- package/src/components/claude-mcp-paste.ts +132 -0
- package/src/components/claude-session-picker.tsx +214 -0
- package/src/components/form-dialog.tsx +75 -0
- package/src/components/launcher-picker.tsx +237 -0
- package/src/components/mcp-util.ts +53 -0
- package/src/components/notebook-generation-popover.tsx +127 -0
- package/src/components/pill.tsx +15 -0
- package/src/components/plugins-panel.tsx +774 -0
- package/src/components/settings-panel.tsx +1631 -0
- package/src/components/skills-panel.tsx +2084 -0
- package/src/handler.ts +51 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +2583 -0
- package/src/markdown-renderer.tsx +153 -0
- package/src/notebook-generation-toolbar.tsx +281 -0
- package/src/notebook-generation.ts +23 -0
- package/src/open-file-refresh-watcher-env.ts +52 -0
- package/src/open-file-refresh-watcher.ts +260 -0
- package/src/shell-utils.ts +10 -0
- package/src/svg.d.ts +4 -0
- package/src/task-target-notebook.ts +37 -0
- package/src/terminal-drag-format.ts +29 -0
- package/src/terminal-drag.ts +382 -0
- package/src/tokens.ts +171 -0
- package/src/tour/tour-anchors.ts +21 -0
- package/src/tour/tour-config.ts +160 -0
- package/src/tour/tour-events.ts +34 -0
- package/src/tour/tour-overlay.tsx +474 -0
- package/src/tour/tour-state.ts +87 -0
- package/src/tour/tour-steps.ts +281 -0
- package/src/utils.ts +455 -0
- package/style/base.css +3238 -0
- package/style/icons/cell-toolbar-bug.svg +5 -0
- package/style/icons/cell-toolbar-chat.svg +5 -0
- package/style/icons/cell-toolbar-sparkle.svg +5 -0
- package/style/icons/claude.svg +1 -0
- package/style/icons/copilot-warning.svg +1 -0
- package/style/icons/copilot.svg +1 -0
- package/style/icons/copy.svg +1 -0
- package/style/icons/openai.svg +1 -0
- package/style/icons/opencode.svg +1 -0
- package/style/icons/sparkles-warning.svg +5 -0
- package/style/icons/sparkles.svg +1 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,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…
|
|
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…
|
|
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
|
+
}
|