@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,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,4 @@
1
+ declare module '*.svg' {
2
+ const value: string; // @ts-ignore
3
+ export default value;
4
+ }
@@ -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
+ }