@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,260 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
4
|
+
import type { Contents } from '@jupyterlab/services';
|
|
5
|
+
|
|
6
|
+
// The watcher only reads `.context` off each yielded widget. Keeping the
|
|
7
|
+
// surface structural (rather than `DocumentWidget`) lets the unit test
|
|
8
|
+
// pass a fake without casting and lets the live env binding apply its
|
|
9
|
+
// own `instanceof DocumentWidget` filter without the type leaking here.
|
|
10
|
+
export interface IRefreshWatcherWidget {
|
|
11
|
+
readonly context: DocumentRegistry.Context | null | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_REFRESH_POLL_INTERVAL_MS = 3000;
|
|
15
|
+
|
|
16
|
+
// Max in-flight Contents.get calls per tick. The Jupyter server runs
|
|
17
|
+
// each request through its own handler thread; with 15 open tabs and
|
|
18
|
+
// unthrottled fan-out we'd pin 15 sockets every poll. Cap at 4 so a
|
|
19
|
+
// heavy-tab user still finishes a tick well under the 3s interval
|
|
20
|
+
// without hammering the server.
|
|
21
|
+
export const DEFAULT_TICK_CONCURRENCY = 4;
|
|
22
|
+
|
|
23
|
+
// Shell areas the live binding walks looking for open DocumentWidgets.
|
|
24
|
+
// 'main' is the primary editor area (including split panes managed by
|
|
25
|
+
// the underlying DockPanel); 'left' and 'right' catch the rare case
|
|
26
|
+
// where a user drags a notebook tab into a sidebar in JL4.
|
|
27
|
+
//
|
|
28
|
+
// 'down' is intentionally absent. It's listed in JupyterLab's
|
|
29
|
+
// TypeScript Area union (application/lib/shell.d.ts) but NOT
|
|
30
|
+
// implemented in LabShell.widgets()'s runtime switch
|
|
31
|
+
// (application/lib/shell.js) — asking for it throws
|
|
32
|
+
// `Invalid area: down`. Reported on PR #330 review; do not re-add
|
|
33
|
+
// without confirming the runtime impl in the installed JL version.
|
|
34
|
+
//
|
|
35
|
+
// The constant lives here (rather than in the env binding) so the
|
|
36
|
+
// test suite can pin its contents without importing
|
|
37
|
+
// @jupyterlab/docregistry, which ships ESM that ts-jest's default
|
|
38
|
+
// transform can't parse.
|
|
39
|
+
export const WATCHED_SHELL_AREAS = ['main', 'left', 'right'] as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Inputs the revert decision depends on. Keeping this pure (no
|
|
43
|
+
* JupyterLab types) lets the unit test pin the policy without
|
|
44
|
+
* instantiating a real Context.
|
|
45
|
+
*/
|
|
46
|
+
export interface IRevertDecisionInputs {
|
|
47
|
+
diskLastModified: string | null | undefined;
|
|
48
|
+
contextLastModified: string | null | undefined;
|
|
49
|
+
isDirty: boolean;
|
|
50
|
+
isReady: boolean;
|
|
51
|
+
isDisposed: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether the open document's in-memory model should be reverted to
|
|
56
|
+
* match the on-disk version. The rules, in order:
|
|
57
|
+
*
|
|
58
|
+
* 1. Skip if the context is gone (disposed) or not yet populated
|
|
59
|
+
* (`isReady` false). Calling `revert()` against an unready
|
|
60
|
+
* context races the initial load.
|
|
61
|
+
* 2. Skip if the user has unsaved local edits (`isDirty`). Silently
|
|
62
|
+
* clobbering their work would be hostile; the standard
|
|
63
|
+
* JupyterLab "newer on disk" prompt will surface on save.
|
|
64
|
+
* 3. Skip if we can't parse either timestamp (either side missing
|
|
65
|
+
* or unparseable).
|
|
66
|
+
* 4. Revert iff disk's `last_modified` parses to a strictly greater
|
|
67
|
+
* epoch ms than the context's last-known value. Equal means
|
|
68
|
+
* already current (a save we initiated, or a no-op re-read).
|
|
69
|
+
*
|
|
70
|
+
* The comparison is numeric, not lexicographic, because jupyter_server
|
|
71
|
+
* serializes `last_modified` via Python's `datetime.isoformat()` which
|
|
72
|
+
* omits the fractional component when `microsecond == 0`. That means
|
|
73
|
+
* the same instant can arrive as `"...:56Z"` or `"...:56.000000Z"`,
|
|
74
|
+
* and `"...:56.000000Z" < "...:56Z"` lexicographically (the `.` at
|
|
75
|
+
* 0x2E sorts below the `Z` at 0x5A at position 19). A string compare
|
|
76
|
+
* would fire a spurious revert when the two sides happen to land on
|
|
77
|
+
* different sides of the fractional/non-fractional boundary for the
|
|
78
|
+
* same mtime. Parsing both through `new Date(s).getTime()` collapses
|
|
79
|
+
* them to the same epoch ms.
|
|
80
|
+
*
|
|
81
|
+
* JupyterLab's own newer-on-disk check (`docregistry/lib/context.js`,
|
|
82
|
+
* see `lastModifiedCheckMargin`) applies a 500ms tolerance to absorb
|
|
83
|
+
* filesystem mtime jitter, but that comparison drives a user-facing
|
|
84
|
+
* "newer on disk, save anyway?" prompt where false-positive is just a
|
|
85
|
+
* dialog. Our comparison drives a silent revert that could clobber an
|
|
86
|
+
* agent's edit that landed within the margin of the user's own save;
|
|
87
|
+
* we deliberately stay strict.
|
|
88
|
+
*/
|
|
89
|
+
export function shouldRevertContext({
|
|
90
|
+
diskLastModified,
|
|
91
|
+
contextLastModified,
|
|
92
|
+
isDirty,
|
|
93
|
+
isReady,
|
|
94
|
+
isDisposed
|
|
95
|
+
}: IRevertDecisionInputs): boolean {
|
|
96
|
+
if (isDisposed || !isReady) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (isDirty) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (!diskLastModified || !contextLastModified) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
// Match JL's own idiom (docregistry/lib/context.js:625-630) and the
|
|
106
|
+
// five other `new Date(...).getTime()` call sites in this codebase.
|
|
107
|
+
// Equivalent to `Date.parse(s)` but consistent with house style.
|
|
108
|
+
const diskMs = new Date(diskLastModified).getTime();
|
|
109
|
+
const contextMs = new Date(contextLastModified).getTime();
|
|
110
|
+
// NaN on either side (unparseable string) makes the comparison
|
|
111
|
+
// false, so a malformed timestamp degrades safely to "don't revert."
|
|
112
|
+
return diskMs > contextMs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Side-effect surface the watcher reaches into. Extracted so tests
|
|
117
|
+
* can pass a thin fake without standing up a real JupyterFrontEnd or
|
|
118
|
+
* Contents singleton.
|
|
119
|
+
*/
|
|
120
|
+
export interface IRefreshWatcherEnv {
|
|
121
|
+
/** Yield every currently-open document widget the watcher should consider. */
|
|
122
|
+
iterDocumentWidgets: () => Iterable<IRefreshWatcherWidget>;
|
|
123
|
+
/** Fetch on-disk metadata without the body (cheap stat-shaped call). */
|
|
124
|
+
fetchDiskModel: (path: string) => Promise<Contents.IModel>;
|
|
125
|
+
/** Set/clear the polling interval. Pulled out for fake timers in tests. */
|
|
126
|
+
setInterval: (handler: () => void, ms: number) => unknown;
|
|
127
|
+
clearInterval: (handle: unknown) => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface IRefreshWatcherOptions {
|
|
131
|
+
env: IRefreshWatcherEnv;
|
|
132
|
+
intervalMs?: number;
|
|
133
|
+
/** Cap on concurrent Contents.get calls per tick. Defaults to DEFAULT_TICK_CONCURRENCY. */
|
|
134
|
+
tickConcurrency?: number;
|
|
135
|
+
/** Re-checked on every tick so a settings toggle takes effect without restart. */
|
|
136
|
+
isEnabled: () => boolean;
|
|
137
|
+
/** Hook for tests / telemetry — fired once per revert (the heavy outcome). */
|
|
138
|
+
onRevert?: (path: string) => void;
|
|
139
|
+
/** Hook for tests / diagnostics — fired when a check throws. */
|
|
140
|
+
onError?: (path: string, error: unknown) => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Polls every open document widget on a fixed cadence, comparing the
|
|
145
|
+
* file's on-disk `last_modified` against the context's last-known
|
|
146
|
+
* value, and calls `context.revert()` when the disk is newer.
|
|
147
|
+
*
|
|
148
|
+
* Why polling at all (when the Contents API exposes a `fileChanged`
|
|
149
|
+
* signal): agents like Claude write directly to the filesystem,
|
|
150
|
+
* bypassing the API. The signal fires for Lab-routed writes only.
|
|
151
|
+
* Polling catches both paths uniformly and keeps the watcher
|
|
152
|
+
* single-purpose.
|
|
153
|
+
*
|
|
154
|
+
* Returns a teardown function the caller invokes on plugin
|
|
155
|
+
* deactivation to stop the timer.
|
|
156
|
+
*/
|
|
157
|
+
export function attachOpenFileRefreshWatcher(
|
|
158
|
+
options: IRefreshWatcherOptions
|
|
159
|
+
): () => void {
|
|
160
|
+
const intervalMs = options.intervalMs ?? DEFAULT_REFRESH_POLL_INTERVAL_MS;
|
|
161
|
+
|
|
162
|
+
let inFlight = false;
|
|
163
|
+
let stopped = false;
|
|
164
|
+
const tick = async (): Promise<void> => {
|
|
165
|
+
// Defense against the browser firing a stale interval handler
|
|
166
|
+
// after we've called clearInterval, and against tests that invoke
|
|
167
|
+
// the captured handler directly post-teardown.
|
|
168
|
+
if (stopped) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!options.isEnabled()) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Re-entrancy guard: a slow Contents.get on one widget shouldn't
|
|
175
|
+
// pile up additional ticks while it resolves. Skip rather than
|
|
176
|
+
// queue so a transient server slowdown doesn't snowball.
|
|
177
|
+
if (inFlight) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
inFlight = true;
|
|
181
|
+
try {
|
|
182
|
+
const seen = new Set<string>();
|
|
183
|
+
const targets: DocumentRegistry.Context[] = [];
|
|
184
|
+
for (const widget of options.env.iterDocumentWidgets()) {
|
|
185
|
+
const context = widget.context;
|
|
186
|
+
if (!context || !context.path) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Dedupe across widgets sharing the same context (split-view,
|
|
190
|
+
// notebook + editor view, etc.); reverting once per path is
|
|
191
|
+
// enough since the context is the shared mutable state.
|
|
192
|
+
if (seen.has(context.path)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
seen.add(context.path);
|
|
196
|
+
targets.push(context);
|
|
197
|
+
}
|
|
198
|
+
// Chunked fan-out: parallelism within a batch (so 4 tabs finish
|
|
199
|
+
// in one RTT instead of four), serialized across batches (so a
|
|
200
|
+
// 20-tab user doesn't pin 20 sockets at once). Per-context
|
|
201
|
+
// errors stay inside checkOneContext via its try/catch, so a
|
|
202
|
+
// Promise.all on the batch can't reject and tear the loop.
|
|
203
|
+
const concurrency = Math.max(
|
|
204
|
+
1,
|
|
205
|
+
options.tickConcurrency ?? DEFAULT_TICK_CONCURRENCY
|
|
206
|
+
);
|
|
207
|
+
for (let i = 0; i < targets.length; i += concurrency) {
|
|
208
|
+
const batch = targets.slice(i, i + concurrency);
|
|
209
|
+
await Promise.all(batch.map(ctx => checkOneContext(ctx, options)));
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
inFlight = false;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const handle = options.env.setInterval(() => {
|
|
217
|
+
// Swallow tick-level errors so a single bad path can't kill the
|
|
218
|
+
// poller; per-context errors are reported via onError above.
|
|
219
|
+
void tick();
|
|
220
|
+
}, intervalMs);
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
stopped = true;
|
|
224
|
+
options.env.clearInterval(handle);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function checkOneContext(
|
|
229
|
+
context: DocumentRegistry.Context,
|
|
230
|
+
options: IRefreshWatcherOptions
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
try {
|
|
233
|
+
const diskModel = await options.env.fetchDiskModel(context.path);
|
|
234
|
+
const decision = shouldRevertContext({
|
|
235
|
+
diskLastModified: diskModel.last_modified,
|
|
236
|
+
contextLastModified: context.contentsModel?.last_modified,
|
|
237
|
+
isDirty: context.model.dirty,
|
|
238
|
+
isReady: context.isReady,
|
|
239
|
+
isDisposed: context.isDisposed
|
|
240
|
+
});
|
|
241
|
+
if (!decision) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Defense in depth: re-read dirty/disposed immediately before the
|
|
245
|
+
// revert call. Today this is strictly belt-and-suspenders — no
|
|
246
|
+
// microtask boundary exists between the dirty read inside
|
|
247
|
+
// shouldRevertContext above and the await on revert() below, so a
|
|
248
|
+
// keystroke cannot land in that window. The re-check survives a
|
|
249
|
+
// future refactor that inserts an await (telemetry, an instrument
|
|
250
|
+
// hook, etc.) between the decision and the revert call without
|
|
251
|
+
// anyone having to re-derive the safety argument.
|
|
252
|
+
if (context.model.dirty || context.isDisposed) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
await context.revert();
|
|
256
|
+
options.onRevert?.(context.path);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
options.onError?.(context.path, error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* POSIX-shell single-quote escape: every embedded single quote is closed,
|
|
5
|
+
* emitted as an escaped literal, and the quote re-opened. The result is
|
|
6
|
+
* safe to splice into a shell command without further sanitization.
|
|
7
|
+
*/
|
|
8
|
+
export function shellSingleQuote(value: string): string {
|
|
9
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
10
|
+
}
|
package/src/svg.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
import { CommandIDs } from './command-ids';
|
|
4
|
+
|
|
5
|
+
// Cell-targeting commands accept an optional `notebookPath` to bypass
|
|
6
|
+
// `app.shell.currentWidget`. When the agent runs a task and the user
|
|
7
|
+
// switches tabs mid-run, the previously-active notebook is no longer the
|
|
8
|
+
// focused widget; the chat sidebar injects the captured task-target path
|
|
9
|
+
// into args for these command IDs so cell ops resolve against the right
|
|
10
|
+
// notebook (issue #252). Built from `CommandIDs` so a rename on either
|
|
11
|
+
// side is caught at compile time.
|
|
12
|
+
export const NOTEBOOK_TARGETED_COMMAND_IDS: ReadonlySet<string> = new Set([
|
|
13
|
+
CommandIDs.addMarkdownCellToActiveNotebook,
|
|
14
|
+
CommandIDs.addCodeCellToActiveNotebook,
|
|
15
|
+
CommandIDs.getCellTypeAndSource,
|
|
16
|
+
CommandIDs.setCellTypeAndSource,
|
|
17
|
+
CommandIDs.getNumberOfCells,
|
|
18
|
+
CommandIDs.getCellOutput,
|
|
19
|
+
CommandIDs.insertCellAtIndex,
|
|
20
|
+
CommandIDs.deleteCellAtIndex,
|
|
21
|
+
CommandIDs.runCellAtIndex
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function injectTaskTargetNotebook(
|
|
25
|
+
commandId: string,
|
|
26
|
+
args: Record<string, unknown> | undefined,
|
|
27
|
+
taskTargetPath: string | null
|
|
28
|
+
): Record<string, unknown> | undefined {
|
|
29
|
+
if (
|
|
30
|
+
!taskTargetPath ||
|
|
31
|
+
!NOTEBOOK_TARGETED_COMMAND_IDS.has(commandId) ||
|
|
32
|
+
(args && args.notebookPath)
|
|
33
|
+
) {
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
return { ...(args ?? {}), notebookPath: taskTargetPath };
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
// Pure helpers extracted from `terminal-drag.ts` so they can be unit
|
|
4
|
+
// tested without JupyterLab / Lumino imports (which pull DOM globals
|
|
5
|
+
// that jsdom doesn't provide, like DragEvent).
|
|
6
|
+
|
|
7
|
+
import { shellSingleQuote } from './shell-utils';
|
|
8
|
+
|
|
9
|
+
export type DragMode = 'mention' | 'raw';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a list of paths for injection per mode. @-mention prefixes each
|
|
13
|
+
* path with "@" (Claude Code syntax, no quoting); raw mode shell-escapes
|
|
14
|
+
* absolute paths for non-Claude shell sessions. Single-space separator
|
|
15
|
+
* is intentional; the trailing space is appended by the caller.
|
|
16
|
+
*/
|
|
17
|
+
export function formatForMode(paths: string[], mode: DragMode): string {
|
|
18
|
+
if (mode === 'mention') {
|
|
19
|
+
return paths.map(p => `@${p}`).join(' ');
|
|
20
|
+
}
|
|
21
|
+
return paths.map(shellSingleQuote).join(' ');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function invertMode(mode: DragMode, shouldInvert: boolean): DragMode {
|
|
25
|
+
if (!shouldInvert) {
|
|
26
|
+
return mode;
|
|
27
|
+
}
|
|
28
|
+
return mode === 'mention' ? 'raw' : 'mention';
|
|
29
|
+
}
|