feat(deepnote-file): Migration to single notebook deepnote file#381
feat(deepnote-file): Migration to single notebook deepnote file#381
Conversation
…mtime conflicts during workspace saves
…out block IDs and implement a valid project structure for testing
…ration result and log warnings for undefined saves
…ngeWatcher to ensure proper handling of snapshot reads
… deserialization logic - Introduced `clearNotebookSelection` method in `DeepnoteNotebookManager` to reset notebook selection for a project. - Updated `DeepnoteFileChangeWatcher` to call `clearNotebookSelection` during file change events, ensuring the active editor is prioritized during re-deserialization. - Modified `deserializeNotebook` method in `DeepnoteNotebookSerializer` to accept an optional `notebookId` parameter, preventing race conditions when multiple notebooks from the same project are open.
…gement - Enhanced `createMockChildProcess` to provide a more comprehensive mock implementation for testing. - Updated `DeepnoteServerStarter` to ensure proper disposal of existing disposables when monitoring server output, improving resource management. - Adjusted error handling in server startup to streamline diagnostics and output tracking.
- Introduced a new `applyNotebookEdits` method in `DeepnoteFileChangeWatcher` to centralize the application of notebook edits, improving code readability and maintainability. - Updated existing calls to `workspace.applyEdit` to utilize the new method, reducing redundancy in the codebase. - Adjusted unit tests to reflect changes in the edit application process, ensuring consistent behavior across the application.
…ling - Introduced `queueNotebookResolution` and `consumePendingNotebookResolution` methods in `DeepnoteNotebookManager` to manage transient notebook resolutions, improving the handling of notebook selections during file operations. - Updated `DeepnoteNotebookSerializer` to prioritize queued notebook resolutions, ensuring more reliable notebook ID retrieval during deserialization. - Refactored `DeepnoteExplorerView` to utilize a new `registerNotebookOpenIntent` method, streamlining the process of selecting and opening notebooks. - Improved error handling in `DeepnoteServerStarter` to log warnings when disposing listeners fails, enhancing diagnostics during server operations. - Adjusted unit tests to cover new functionality and ensure consistent behavior across notebook management processes.
…n DeepnoteFileChangeWatcher
…pnoteNotebookManager - Added new unit tests for self-write detection in `DeepnoteFileChangeWatcher`, ensuring that saving a deepnote notebook suppresses subsequent file change events. - Implemented tests in `DeepnoteNotebookManager` to verify project data updates without altering the current notebook ID, ensuring data integrity during updates. - Introduced a utility function for creating notebook documents in tests, improving test setup consistency. - Expanded multi-notebook save scenarios in `DeepnoteNotebookSerializer` to validate notebook ID resolution during serialization and deserialization processes.
…Watcher - Introduced new unit tests to validate snapshot changes and deserialization interactions in `DeepnoteFileChangeWatcher`. - Enhanced test setup to capture interactions with notebook edits, ensuring accurate application of changes across multi-notebook projects. - Improved organization of imports and added missing type definitions for better clarity and maintainability.
…pnoteFileChangeWatcher - Updated `applyNotebookEdits` calls to handle failures in restoring block IDs, logging warnings when restoration fails after execution API and replaceCells operations. - Added unit tests to verify that saving does not occur when metadata restoration fails, ensuring data integrity during notebook edits. - Improved test coverage for scenarios involving metadata restoration failures, enhancing the reliability of the DeepnoteFileChangeWatcher functionality.
…book details in the status bar - Introduced `DeepnoteNotebookInfoStatusBar` to show the active Deepnote notebook name and provide a copy action for notebook details. - Updated service registration in both `serviceRegistry.node.ts` and `serviceRegistry.web.ts` to include the new status bar service. - Added a new command `CopyNotebookDetails` to facilitate copying notebook information to the clipboard.
- Introduced a new command `deepnote.copyNotebookDetails` to allow users to copy details of the active Deepnote notebook. - Updated localization file to include the title for the new command, enhancing user experience with clear labeling.
- Added support for resolving notebook IDs from open tabs, improving the handling of notebook selections during reloads. - Introduced a new method `findNotebookIdsFromTabs` to extract notebook IDs from the current tab groups. - Updated `findCurrentNotebookId` to prioritize tab-based resolution when available, alongside existing resolution strategies. - Enhanced unit tests to cover various scenarios for tab-based resolution, ensuring robust functionality.
…atch resolution - Removed the `selectNotebookForProject` and `clearNotebookSelection` methods from `DeepnoteNotebookManager` to simplify notebook selection logic. - Introduced a new mechanism in `DeepnoteActivationService` to check and fix notebook ID mismatches when opening documents, improving the reliability of notebook loading. - Updated `DeepnoteSerializer` to prioritize current notebook IDs and handle active tab resolutions more effectively. - Enhanced unit tests to cover the new mismatch resolution logic and ensure proper functionality across various scenarios.
…tebook ID resolution - Removed unused variables and methods related to mismatch checking in `DeepnoteActivationService`, streamlining the activation process. - Updated `findCurrentNotebookId` in `DeepnoteSerializer` to improve notebook ID resolution logic, prioritizing active tab detection. - Adjusted unit tests to reflect changes in notebook ID resolution, ensuring accurate behavior when no pending resolutions exist.
…unused logic - Eliminated unnecessary variables and methods related to recent serialization tracking in `DeepnoteNotebookSerializer`, simplifying the notebook ID resolution process. - Updated the logic for finding the current notebook ID to focus on active tab detection, enhancing overall efficiency. - Removed outdated unit tests that were no longer relevant to the current implementation, ensuring test suite accuracy.
…oteNotebookManager - Introduced debug logging in `DeepnoteNotebookManager` to track valid pending notebook resolutions, enhancing traceability during resolution processes. - Simplified block iteration in `DeepnoteNotebookSerializer` by removing unnecessary null checks, improving code clarity and performance.
- Added a new event listener in `DeepnoteActivationService` to verify deserialized notebooks upon opening. - Improved `DeepnoteNotebookSerializer` to handle cases where no notebook ID is resolved, returning an empty state instead of throwing an error. - Updated unit tests to reflect changes in notebook deserialization behavior, ensuring accurate handling of scenarios with no available notebooks.
📝 WalkthroughWalkthroughThis PR adds DeepnoteAutoSplitter (splits multi-notebook .deepnote files into per-notebook files and corresponding snapshot files), threads notebookId through serializer/manager/snapshot flows, reorganizes the explorer to group files by projectId and change open/create behaviors to use sibling files, adds a status-bar service and the Sequence Diagram(s)sequenceDiagram
participant User
participant VSCode as "VS Code"
participant Activation as "DeepnoteActivationService"
participant AutoSplitter as "DeepnoteAutoSplitter"
participant FS as "FileSystem"
participant Snapshot as "SnapshotService"
participant Explorer as "DeepnoteExplorerView"
User->>VSCode: open Deepnote notebook
VSCode->>Activation: onDidOpenNotebookDocument(deepnote)
Activation->>Activation: deserializeDeepnoteFile(uri)
Activation->>AutoSplitter: splitIfNeeded(uri, deepnoteFile)
AutoSplitter->>AutoSplitter: detect multiple non-init notebooks
alt multiple notebooks
AutoSplitter->>FS: write new .deepnote files per notebook
AutoSplitter->>Snapshot: splitSnapshots(projectId, notebookIds)
Snapshot->>FS: write per-notebook snapshot files
Snapshot->>FS: delete old combined snapshot
AutoSplitter->>FS: update original file (keep primary)
AutoSplitter-->>Activation: return {wasSplit: true, newFiles}
Activation->>Explorer: refreshTree()
else single or none
AutoSplitter-->>Activation: return {wasSplit: false}
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #381 +/- ##
===========================
===========================
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/notebooks/deepnote/deepnoteTreeDataProvider.ts (1)
290-307:⚠️ Potential issue | 🟠 MajorFilter out the init notebook here too.
This path reintroduces
project.project.initNotebookId, so grouped multi-notebook files will show an internal init notebook as a normal explorer child.Proposed fix
private getNotebooksForProject(projectItem: DeepnoteTreeItem): DeepnoteTreeItem[] { const project = projectItem.data as DeepnoteProject; - const notebooks = project.project.notebooks || []; + const initNotebookId = project.project.initNotebookId; + const notebooks = (project.project.notebooks || []).filter((notebook) => notebook.id !== initNotebookId); return notebooks.map((notebook: DeepnoteNotebook) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteTreeDataProvider.ts` around lines 290 - 307, getNotebooksForProject currently returns all notebooks including the project's init notebook (project.project.initNotebookId), which causes the internal init notebook to appear as a normal child; modify getNotebooksForProject to filter out any notebook whose id matches project.project.initNotebookId before mapping to DeepnoteTreeItem (i.e., compute notebooks.filter(n => n.id !== project.project.initNotebookId) and then map that filtered list to new DeepnoteTreeItem instances using the existing context construction and TreeItemCollapsibleState.None).src/notebooks/deepnote/deepnoteTreeDataProvider.unit.test.ts (1)
538-593:⚠️ Potential issue | 🟡 MinorRename these tests to match what they assert.
They no longer verify sorting; they verify that notebook order is preserved. Keeping “should sort...” in the names will mislead readers and make regressions harder to spot.
Also applies to: 595-650
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteTreeDataProvider.unit.test.ts` around lines 538 - 593, Rename the test case titles so they accurately describe behavior being asserted: update the test with description "should sort notebooks alphabetically by name within a project" (and the similar test around lines 595-650) to something like "should preserve original notebook order within a project" or "should return notebooks in original order" so the test name matches the assertions in DeepnoteTreeDataProvider unit tests that call provider.getChildren and verify the original order of notebook items; update only the test description strings for the test(...) calls (e.g., the one constructing mockProjectWithNotebooks and mockProjectItem) to avoid misleading readers.src/notebooks/deepnote/deepnoteFileChangeWatcher.ts (1)
270-286:⚠️ Potential issue | 🟠 Major
notebookIdis extracted, then discarded.
handleSnapshotFileChange()derives anotebookId, and this method filters editors with it, but the queued operation only keepsprojectId. The downstream read still uses project-only snapshot lookup, so notebook-scoped snapshot files will not update the matching editor reliably.Also applies to: 627-639
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts` around lines 270 - 286, enqueueSnapshotOutputUpdate currently filters by notebookId but only stores projectId in the pending operation, causing notebook-scoped snapshot lookups to be lost; update the pending operation stored in pendingOperations (keyed by nbKey) to include notebookId (e.g., set { type: 'snapshot-output-update', projectId, notebookId }) and ensure any code that reads this pending op (including drainQueue and the similar block referenced around the second occurrence) uses the notebookId when performing snapshot lookup/updates so notebook-scoped snapshot files update the correct editor.src/notebooks/deepnote/snapshots/snapshotService.ts (1)
401-445:⚠️ Potential issue | 🟠 MajorNotebook-scoped reads dropped backward compatibility.
When
notebookIdis provided, this only searches*_${projectId}_${notebookId}_.... Existing legacy snapshots named*_${projectId}_...will stop restoring outputs after the migration unless the user reruns cells and generates new files.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/snapshots/snapshotService.ts` around lines 401 - 445, readSnapshot currently loses backward compatibility when notebookId is provided because latestGlob and timestampedGlob only match filenames containing _{projectId}_{notebookId}_..., skipping legacy files named *_{{projectId}}_...; update readSnapshot to try both notebook-scoped and legacy patterns when notebookId is present by constructing an additional legacy glob (e.g., `**/snapshots/*_${projectId}_...`) and searching with both patterns inside the existing workspace.findFiles loop(s), falling back to the legacy match if no notebook-scoped file is found; ensure the same dual-pattern logic is applied for both the "latest" search (latestGlob) and the timestamped search (timestampedGlob) before calling parseSnapshotFile so parseSnapshotFile, RelativePattern, and workspace.findFiles are reused without changing their APIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 405-414: Extract the duplicated disposal logic into a single
helper method named disposeOutputListeners(fileKey): it should look up
this.disposablesByFile.get(fileKey), iterate disposables, call dispose() inside
a try/catch, and log warnings on exceptions (same behavior as current block).
Replace the duplicated blocks in monitorServerOutput, stopServerForEnvironment,
and dispose() to call this.disposeOutputListeners(fileKey) and ensure the helper
handles the case of no entry and leaves map state consistent. Keep the helper as
a private method on the same class and reuse it everywhere the original disposal
loop appears.
In `@src/kernels/deepnote/deepnoteTestHelpers.node.ts`:
- Around line 15-66: Replace the hand-rolled mock object with one that extends
Node's EventEmitter and uses real emitter methods
(emit/on/once/removeListener/listeners/etc.) and mutable internal state exposed
via getters for properties like killed and connected; specifically update the
mock that currently defines killed, connected, signalCode, spawnargs, spawnfile
and methods kill, send, disconnect, unref, ref and all the listener stubs so the
mock class (or factory) wraps EventEmitter, maintains internal booleans (e.g.,
_killed, _connected) that are updated by kill/disconnect/ref/unref and exposed
as getters killed and connected, forward listener methods to the EventEmitter
instance (or inherit them) and remove the hardcoded stub implementations and the
Symbol.dispose no-op in favor of proper EventEmitter behavior.
In `@src/notebooks/deepnote/deepnoteActivationService.ts`:
- Around line 59-63: The current registration of
workspace.onDidOpenNotebookDocument only handles future opens and misses
notebooks already open at activation; after you push the
onDidOpenNotebookDocument listener (or inside activate where
extensionContext.subscriptions is set up), iterate over
workspace.notebookDocuments and call the existing checkAndSplitIfNeeded(doc) for
each open notebook to ensure restored/.deepnote notebooks are processed—use the
same method name checkAndSplitIfNeeded and keep adding the subscriptions onto
extensionContext.subscriptions as appropriate.
In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts`:
- Around line 133-160: snapshotGlob currently matches both legacy and new
snapshot filename forms but extractVariantFromSnapshotFilename only understands
the legacy shape, causing filenames like
{slug}_{projectId}_{notebookId}_{variant}.snapshot.deepnote to be mis-parsed and
re-suffixed; before calling splitSingleSnapshot for each snapshotUri in
allSnapshotFiles, detect and skip files that already match the new format (or
extend extractVariantFromSnapshotFilename to parse the new pattern) by checking
the filename parts for the extra notebookId segment (use snapshotUri.fsPath or
basename), and only call splitSingleSnapshot when the file is in the legacy
shape; update the loop around splitSingleSnapshot (and the identical block at
the other occurrence) to perform this guard so valid new-format snapshots are
left untouched.
- Around line 46-49: The filename generation for extraNotebooks (inside the loop
using slugifyNotebookName, originalStem, and Uri.joinPath) can collide when
notebookSlug is identical; update the logic to produce collision-proof names by
including a unique identifier (e.g., notebook.id or an incrementing index/UUID)
or by checking for existing target paths and appending a disambiguator until the
path is unique before writing; apply the same fix to the similar block
referenced around lines 283-289 so newFileName/ newFileUri never overwrite
earlier notebooks.
- Around line 120-290: The private methods in this class are out of order;
reorder them to follow the repo TS file-order rule (grouped by accessibility
then alphabetically). Move the private methods so they appear in alphabetical
order: computeSnapshotHash, extractVariantFromSnapshotFilename, getFileStem,
slugifyNotebookName, splitSingleSnapshot, splitSnapshots (use these exact
function names to locate each method) and leave their implementations unchanged.
In `@src/notebooks/deepnote/deepnoteExplorerView.ts`:
- Around line 523-525: The tree item always opens the default notebook because
context.notebookId is ignored; fix the open logic in deepnoteExplorerView by
including context.notebookId when resolving the notebook (instead of just
Uri.file(context.filePath)). Pass the notebook identifier through to
workspace.openNotebookDocument (or construct a notebook URI using the
fragment/query) or call deserializeNotebook with the notebookId so the correct
notebook instance is opened; update the code paths around fileUri,
workspace.openNotebookDocument and deserializeNotebook to use
context.notebookId.
- Around line 79-80: The issue is that after creating or duplicating a notebook
the code appends the new notebook but reopens only the fileUri (e.g. via
saveProjectAndOpenNotebook(fileUri,...)), which lets
DeepnoteNotebookSerializer.findDefaultNotebook() pick the wrong notebook; modify
the create/duplicate flows to capture the new notebook's identifier (e.g. the
created/duplicated notebook object or its id/path) and pass that specific
notebook target into the reopen call (either by extending
saveProjectAndOpenNotebook to accept a notebookId/notebookUri parameter or by
calling whatever openNotebook/openEditor function exists with the precise
notebook target) so the editor opens the newly created/duplicated notebook
rather than the file's default. Ensure you update all affected call sites (the
create flow, duplicate flow and the other spots noted) to forward the new
notebook identifier.
In `@src/notebooks/deepnote/deepnoteExplorerView.unit.test.ts`:
- Around line 189-190: The test creates two DeepnoteExplorerView instances
(view1 and view2) which each construct a DeepnoteTreeDataProvider that registers
file watchers; to avoid leaking watchers, dispose of these views at the end of
the test (or add an afterEach/tearDown) by calling their dispose method (or
otherwise invoking the DeepnoteExplorerView cleanup) so both view1 and view2 are
properly torn down; locate usages of DeepnoteExplorerView in the test and ensure
view1.dispose() and view2.dispose() (or centralized cleanup) are invoked.
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 757-759: The test "should update outputs when snapshot file
changes" (and/or add a new unit test in deepnoteFileChangeWatcher.unit.test.ts)
currently only uses the legacy snapshot filename pattern produced by testFileUri
and createMockNotebook; update that test or add one to also exercise the new
per-notebook snapshot filename pattern produced by snapshotService (the
..._{projectId}_{notebookId}_{variant}.snapshot.deepnote shape) by calling
testFileUri with the new filename (including notebookId) and asserting the same
"happy-path/self-write" behavior so the watcher reacts as expected; reference
the existing test name and helpers (testFileUri, createMockNotebook) to locate
where to add the additional case.
In `@src/notebooks/deepnote/deepnoteNotebookManager.ts`:
- Around line 47-60: storeOriginalProject and updateOriginalProject both write
cloned DeepnoteProject objects into a single Map (originalProjects) keyed only
by projectId, causing different .deepnote files within the same project to
collide; change the cache to be per-file (either use a composite key like
`${projectId}:${fileId}` where fileId is the unique notebook/.deepnote filename
or switch originalProjects to a nested Map keyed first by projectId then by
fileId) and update both methods (storeOriginalProject, updateOriginalProject) to
use that per-file key, ensuring any other lookups that read originalProjects
also use the same composite or nested-key pattern so each notebook's cached
project is stored separately.
In `@src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts`:
- Around line 224-226: The inline comment above the test that says "Use
updateOriginalProject which doesn't set currentNotebookId" is stale; remove or
update that comment to reflect the new behavior being tested (e.g., that
updateOriginalProject is used and integrations should be updated and true
returned). Locate the comment near the test invoking
manager.updateOriginalProject('project-123', mockProject) and either delete the
sentence about currentNotebookId or replace it with a brief accurate description
of what the test now verifies.
In `@src/notebooks/deepnote/deepnoteTreeDataProvider.ts`:
- Around line 148-150: performInitialScan currently calls loadAllProjectFiles
and then the root render triggers getProjectGroups which repeats the same file
walk; to fix, capture the LoadedProjectFile[] result from loadAllProjectFiles in
performInitialScan (e.g. assign to a new class field like
this.loadedProjectFiles) and have getProjectGroups and any subsequent
group-building logic use that cached array instead of calling
loadAllProjectFiles again. Remove duplicate calls to loadAllProjectFiles found
around getProjectGroups, and add cache invalidation/update logic tied to file
system or workspace change handlers so the cached LoadedProjectFile[] stays in
sync.
In `@src/notebooks/deepnote/deepnoteTreeItem.ts`:
- Around line 78-79: The label construction uses
this.context.filePath.split('/') which fails on Windows; replace split('/')
usages with a platform-aware basename (e.g., import path from 'path' and call
path.basename(this.context.filePath)) when setting this.label and at the other
identical occurrence where filePath is parsed; ensure you fallback to
project.project.name || 'Untitled Project' exactly as before.
In `@src/test/mocks/vsc/extHostedTypes.ts`:
- Around line 2547-2554: The class TabInputNotebook declares two public readonly
properties out of alphabetical order; reorder the member declarations so public
readonly notebookType appears before public readonly uri to satisfy the repo
member-ordering rule, and keep the constructor (constructor(uri: vscUri.URI,
notebookType: string)) assignments consistent by assigning this.notebookType and
this.uri after reordering the property declarations.
---
Outside diff comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 270-286: enqueueSnapshotOutputUpdate currently filters by
notebookId but only stores projectId in the pending operation, causing
notebook-scoped snapshot lookups to be lost; update the pending operation stored
in pendingOperations (keyed by nbKey) to include notebookId (e.g., set { type:
'snapshot-output-update', projectId, notebookId }) and ensure any code that
reads this pending op (including drainQueue and the similar block referenced
around the second occurrence) uses the notebookId when performing snapshot
lookup/updates so notebook-scoped snapshot files update the correct editor.
In `@src/notebooks/deepnote/deepnoteTreeDataProvider.ts`:
- Around line 290-307: getNotebooksForProject currently returns all notebooks
including the project's init notebook (project.project.initNotebookId), which
causes the internal init notebook to appear as a normal child; modify
getNotebooksForProject to filter out any notebook whose id matches
project.project.initNotebookId before mapping to DeepnoteTreeItem (i.e., compute
notebooks.filter(n => n.id !== project.project.initNotebookId) and then map that
filtered list to new DeepnoteTreeItem instances using the existing context
construction and TreeItemCollapsibleState.None).
In `@src/notebooks/deepnote/deepnoteTreeDataProvider.unit.test.ts`:
- Around line 538-593: Rename the test case titles so they accurately describe
behavior being asserted: update the test with description "should sort notebooks
alphabetically by name within a project" (and the similar test around lines
595-650) to something like "should preserve original notebook order within a
project" or "should return notebooks in original order" so the test name matches
the assertions in DeepnoteTreeDataProvider unit tests that call
provider.getChildren and verify the original order of notebook items; update
only the test description strings for the test(...) calls (e.g., the one
constructing mockProjectWithNotebooks and mockProjectItem) to avoid misleading
readers.
In `@src/notebooks/deepnote/snapshots/snapshotService.ts`:
- Around line 401-445: readSnapshot currently loses backward compatibility when
notebookId is provided because latestGlob and timestampedGlob only match
filenames containing _{projectId}_{notebookId}_..., skipping legacy files named
*_{{projectId}}_...; update readSnapshot to try both notebook-scoped and legacy
patterns when notebookId is present by constructing an additional legacy glob
(e.g., `**/snapshots/*_${projectId}_...`) and searching with both patterns
inside the existing workspace.findFiles loop(s), falling back to the legacy
match if no notebook-scoped file is found; ensure the same dual-pattern logic is
applied for both the "latest" search (latestGlob) and the timestamped search
(timestampedGlob) before calling parseSnapshotFile so parseSnapshotFile,
RelativePattern, and workspace.findFiles are reused without changing their APIs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c9598c5c-28d4-4c7f-98d8-4d917f6120bb
📒 Files selected for processing (28)
build/mocha-esm-loader.jspackage.jsonpackage.nls.jsonsrc/kernels/deepnote/deepnoteServerStarter.node.tssrc/kernels/deepnote/deepnoteTestHelpers.node.tssrc/notebooks/deepnote/deepnoteActivationService.tssrc/notebooks/deepnote/deepnoteAutoSplitter.tssrc/notebooks/deepnote/deepnoteExplorerView.tssrc/notebooks/deepnote/deepnoteExplorerView.unit.test.tssrc/notebooks/deepnote/deepnoteFileChangeWatcher.tssrc/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.tssrc/notebooks/deepnote/deepnoteNotebookInfoStatusBar.tssrc/notebooks/deepnote/deepnoteNotebookManager.tssrc/notebooks/deepnote/deepnoteNotebookManager.unit.test.tssrc/notebooks/deepnote/deepnoteSerializer.tssrc/notebooks/deepnote/deepnoteSerializer.unit.test.tssrc/notebooks/deepnote/deepnoteTreeDataProvider.tssrc/notebooks/deepnote/deepnoteTreeDataProvider.unit.test.tssrc/notebooks/deepnote/deepnoteTreeItem.tssrc/notebooks/deepnote/deepnoteTreeItem.unit.test.tssrc/notebooks/deepnote/snapshots/snapshotFiles.tssrc/notebooks/deepnote/snapshots/snapshotService.tssrc/notebooks/serviceRegistry.node.tssrc/notebooks/serviceRegistry.web.tssrc/notebooks/types.tssrc/platform/common/constants.tssrc/test/mocks/vsc/extHostedTypes.tssrc/test/vscode-mock.ts
| killed: false, | ||
| connected: false, | ||
| signalCode: null, | ||
| spawnargs: [], | ||
| spawnfile: '', | ||
| kill: () => true, | ||
| send: () => true, | ||
| disconnect: () => true, | ||
| unref: () => true, | ||
| ref: () => true, | ||
| addListener: function () { | ||
| return this; | ||
| }, | ||
| emit: () => true, | ||
| on: function () { | ||
| return this; | ||
| }, | ||
| once: function () { | ||
| return this; | ||
| }, | ||
| removeListener: function () { | ||
| return this; | ||
| }, | ||
| removeAllListeners: function () { | ||
| return this; | ||
| }, | ||
| prependListener: function () { | ||
| return this; | ||
| }, | ||
| prependOnceListener: function () { | ||
| return this; | ||
| }, | ||
| [Symbol.dispose]: () => { | ||
| return undefined; | ||
| }, | ||
| off: function () { | ||
| return this; | ||
| }, | ||
| setMaxListeners: function () { | ||
| return this; | ||
| }, | ||
| getMaxListeners: () => 10, | ||
| listeners: function () { | ||
| return []; | ||
| }, | ||
| rawListeners: function () { | ||
| return []; | ||
| }, | ||
| eventNames: function () { | ||
| return []; | ||
| }, | ||
| listenerCount: () => 0, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
usage_files="$(rg -l --type=ts '\bcreateMockChildProcess\s*\(' || true)"
if [ -z "$usage_files" ]; then
echo "No call sites found."
exit 0
fi
echo "createMockChildProcess call sites:"
printf '%s\n' "$usage_files"
echo
echo "Potential event/state-dependent usages to inspect:"
while IFS= read -r file; do
rg -n -C2 '\bcreateMockChildProcess\s*\(|\.(on|once|emit|kill|disconnect)\s*\(|\.(killed|connected)\b' "$file"
done <<< "$usage_files"Repository: deepnote/vscode-deepnote
Length of output: 2366
🏁 Script executed:
# Check how mockProcess is actually used in the test files
for file in src/notebooks/deepnote/deepnoteKernelAutoSelector.node.unit.test.ts \
src/kernels/deepnote/environments/deepnoteEnvironmentTreeDataProvider.unit.test.ts \
src/kernels/deepnote/deepnoteLspClientManager.node.vscode.test.ts; do
if [ -f "$file" ]; then
echo "=== $file ==="
# Look for any usage of killed, connected, on, once, emit, kill, disconnect on the process mock
rg -n '(\.killed|\.connected|\.on\(|\.once\(|\.emit\(|\.kill\(|\.disconnect\()\b' "$file" || echo " (no state/event usage found)"
fi
doneRepository: deepnote/vscode-deepnote
Length of output: 401
Refactor mock to use real EventEmitter and mutable state.
The mock's listener stubs and hardcoded killed/connected flags don't match ChildProcess semantics. While current tests don't use these features, a test that awaits process events or checks post-call state will get a lying mock. Use EventEmitter as a base and expose state via getters instead.
Suggested fix
+import { EventEmitter } from 'node:events';
import type { ChildProcess } from 'node:child_process';
export function createMockChildProcess(overrides?: Partial<ChildProcess>): ChildProcess {
- const mockProcess: ChildProcess = {
+ const emitter = new EventEmitter();
+ let killed = false;
+ let connected = false;
+
+ const mockProcess = Object.assign(emitter, {
pid: undefined,
stdio: [null, null, null, null, null],
stdin: null,
stdout: null,
stderr: null,
exitCode: null,
- killed: false,
- connected: false,
+ get killed() {
+ return killed;
+ },
+ get connected() {
+ return connected;
+ },
signalCode: null,
spawnargs: [],
spawnfile: '',
- kill: () => true,
+ kill: () => {
+ killed = true;
+ return true;
+ },
send: () => true,
- disconnect: () => true,
- unref: () => true,
- ref: () => true,
- addListener: function () {
- return this;
- },
- emit: () => true,
- on: function () {
- return this;
- },
- once: function () {
- return this;
- },
- removeListener: function () {
- return this;
- },
- removeAllListeners: function () {
- return this;
- },
- prependListener: function () {
- return this;
- },
- prependOnceListener: function () {
- return this;
+ disconnect: () => {
+ connected = false;
},
[Symbol.dispose]: () => {
- return undefined;
- },
- off: function () {
- return this;
- },
- setMaxListeners: function () {
- return this;
- },
- getMaxListeners: () => 10,
- listeners: function () {
- return [];
- },
- rawListeners: function () {
- return [];
- },
- eventNames: function () {
- return [];
- },
- listenerCount: () => 0,
+ killed = true;
+ },
...overrides
- };
+ }) as ChildProcess;
return mockProcess;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| killed: false, | |
| connected: false, | |
| signalCode: null, | |
| spawnargs: [], | |
| spawnfile: '', | |
| kill: () => true, | |
| send: () => true, | |
| disconnect: () => true, | |
| unref: () => true, | |
| ref: () => true, | |
| addListener: function () { | |
| return this; | |
| }, | |
| emit: () => true, | |
| on: function () { | |
| return this; | |
| }, | |
| once: function () { | |
| return this; | |
| }, | |
| removeListener: function () { | |
| return this; | |
| }, | |
| removeAllListeners: function () { | |
| return this; | |
| }, | |
| prependListener: function () { | |
| return this; | |
| }, | |
| prependOnceListener: function () { | |
| return this; | |
| }, | |
| [Symbol.dispose]: () => { | |
| return undefined; | |
| }, | |
| off: function () { | |
| return this; | |
| }, | |
| setMaxListeners: function () { | |
| return this; | |
| }, | |
| getMaxListeners: () => 10, | |
| listeners: function () { | |
| return []; | |
| }, | |
| rawListeners: function () { | |
| return []; | |
| }, | |
| eventNames: function () { | |
| return []; | |
| }, | |
| listenerCount: () => 0, | |
| import { EventEmitter } from 'node:events'; | |
| import type { ChildProcess } from 'node:child_process'; | |
| export function createMockChildProcess(overrides?: Partial<ChildProcess>): ChildProcess { | |
| const emitter = new EventEmitter(); | |
| let killed = false; | |
| let connected = false; | |
| const mockProcess = Object.assign(emitter, { | |
| pid: undefined, | |
| stdio: [null, null, null, null, null], | |
| stdin: null, | |
| stdout: null, | |
| stderr: null, | |
| exitCode: null, | |
| get killed() { | |
| return killed; | |
| }, | |
| get connected() { | |
| return connected; | |
| }, | |
| signalCode: null, | |
| spawnargs: [], | |
| spawnfile: '', | |
| kill: () => { | |
| killed = true; | |
| return true; | |
| }, | |
| send: () => true, | |
| disconnect: () => { | |
| connected = false; | |
| }, | |
| [Symbol.dispose]: () => { | |
| killed = true; | |
| }, | |
| ...overrides | |
| }) as ChildProcess; | |
| return mockProcess; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/kernels/deepnote/deepnoteTestHelpers.node.ts` around lines 15 - 66,
Replace the hand-rolled mock object with one that extends Node's EventEmitter
and uses real emitter methods (emit/on/once/removeListener/listeners/etc.) and
mutable internal state exposed via getters for properties like killed and
connected; specifically update the mock that currently defines killed,
connected, signalCode, spawnargs, spawnfile and methods kill, send, disconnect,
unref, ref and all the listener stubs so the mock class (or factory) wraps
EventEmitter, maintains internal booleans (e.g., _killed, _connected) that are
updated by kill/disconnect/ref/unref and exposed as getters killed and
connected, forward listener methods to the EventEmitter instance (or inherit
them) and remove the hardcoded stub implementations and the Symbol.dispose no-op
in favor of proper EventEmitter behavior.
| this.extensionContext.subscriptions.push( | ||
| workspace.onDidOpenNotebookDocument((doc) => { | ||
| void this.checkAndSplitIfNeeded(doc); | ||
| }) | ||
| ); |
There was a problem hiding this comment.
Handle notebooks that are already open when activation runs.
This listener only covers future opens. Restored/open .deepnote notebooks that exist before activate() finishes will never hit checkAndSplitIfNeeded(), so multi-notebook files can stay unsplit until the user closes and reopens them.
Possible fix
this.extensionContext.subscriptions.push(
workspace.onDidOpenNotebookDocument((doc) => {
void this.checkAndSplitIfNeeded(doc);
})
);
+
+ for (const doc of workspace.notebookDocuments) {
+ void this.checkAndSplitIfNeeded(doc);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.extensionContext.subscriptions.push( | |
| workspace.onDidOpenNotebookDocument((doc) => { | |
| void this.checkAndSplitIfNeeded(doc); | |
| }) | |
| ); | |
| this.extensionContext.subscriptions.push( | |
| workspace.onDidOpenNotebookDocument((doc) => { | |
| void this.checkAndSplitIfNeeded(doc); | |
| }) | |
| ); | |
| for (const doc of workspace.notebookDocuments) { | |
| void this.checkAndSplitIfNeeded(doc); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteActivationService.ts` around lines 59 - 63,
The current registration of workspace.onDidOpenNotebookDocument only handles
future opens and misses notebooks already open at activation; after you push the
onDidOpenNotebookDocument listener (or inside activate where
extensionContext.subscriptions is set up), iterate over
workspace.notebookDocuments and call the existing checkAndSplitIfNeeded(doc) for
each open notebook to ensure restored/.deepnote notebooks are processed—use the
same method name checkAndSplitIfNeeded and keep adding the subscriptions onto
extensionContext.subscriptions as appropriate.
| for (const notebook of extraNotebooks) { | ||
| const notebookSlug = this.slugifyNotebookName(notebook.name); | ||
| const newFileName = `${originalStem}_${notebookSlug}.deepnote`; | ||
| const newFileUri = Uri.joinPath(parentDir, newFileName); |
There was a problem hiding this comment.
Make split filenames collision-proof.
notebookSlug is not unique. Two notebooks with the same slug — or any names that hit the 'notebook' fallback — will write the same path and silently overwrite each other during migration.
Proposed fix
- const newFileName = `${originalStem}_${notebookSlug}.deepnote`;
+ const newFileName = `${originalStem}_${notebookSlug}_${notebook.id}.deepnote`;Also applies to: 283-289
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts` around lines 46 - 49, The
filename generation for extraNotebooks (inside the loop using
slugifyNotebookName, originalStem, and Uri.joinPath) can collide when
notebookSlug is identical; update the logic to produce collision-proof names by
including a unique identifier (e.g., notebook.id or an incrementing index/UUID)
or by checking for existing target paths and appending a disambiguator until the
path is unique before writing; apply the same fix to the similar block
referenced around lines 283-289 so newFileName/ newFileUri never overwrite
earlier notebooks.
| private async splitSnapshots( | ||
| _projectFileUri: Uri, | ||
| projectId: string, | ||
| projectName: string, | ||
| primaryNotebookId: string, | ||
| extraNotebookIds: string[] | ||
| ): Promise<void> { | ||
| const workspaceFolders = workspace.workspaceFolders; | ||
|
|
||
| if (!workspaceFolders || workspaceFolders.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const snapshotGlob = `**/snapshots/*_${projectId}_*.snapshot.deepnote`; | ||
| let allSnapshotFiles: Uri[] = []; | ||
|
|
||
| for (const folder of workspaceFolders) { | ||
| const pattern = new RelativePattern(folder, snapshotGlob); | ||
| const files = await workspace.findFiles(pattern, null, 100); | ||
| allSnapshotFiles = allSnapshotFiles.concat(files); | ||
| } | ||
|
|
||
| if (allSnapshotFiles.length === 0) { | ||
| logger.debug(`[AutoSplitter] No snapshots found for project ${projectId}`); | ||
| return; | ||
| } | ||
|
|
||
| let slug: string; | ||
|
|
||
| try { | ||
| slug = slugifyProjectName(projectName); | ||
| } catch { | ||
| logger.warn(`[AutoSplitter] Cannot slugify project name, skipping snapshot split`); | ||
| return; | ||
| } | ||
|
|
||
| const allNotebookIds = [primaryNotebookId, ...extraNotebookIds]; | ||
|
|
||
| for (const snapshotUri of allSnapshotFiles) { | ||
| try { | ||
| await this.splitSingleSnapshot(snapshotUri, slug, projectId, allNotebookIds); | ||
| } catch (error) { | ||
| logger.warn(`[AutoSplitter] Failed to split snapshot ${snapshotUri.path}`, error); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private async splitSingleSnapshot( | ||
| snapshotUri: Uri, | ||
| slug: string, | ||
| projectId: string, | ||
| notebookIds: string[] | ||
| ): Promise<void> { | ||
| const content = await workspace.fs.readFile(snapshotUri); | ||
| const { deserializeDeepnoteFile: parseFile } = await import('@deepnote/blocks'); | ||
| const snapshotData = parseFile(new TextDecoder().decode(content)); | ||
|
|
||
| const snapshotDir = Uri.joinPath(snapshotUri, '..'); | ||
|
|
||
| // Extract variant from existing filename | ||
| const basename = snapshotUri.path.split('/').pop() ?? ''; | ||
| const variant = this.extractVariantFromSnapshotFilename(basename, projectId); | ||
|
|
||
| if (!variant) { | ||
| logger.debug(`[AutoSplitter] Could not extract variant from ${basename}, skipping`); | ||
| return; | ||
| } | ||
|
|
||
| // Create per-notebook snapshot files | ||
| for (const notebookId of notebookIds) { | ||
| const notebookData = structuredClone(snapshotData); | ||
|
|
||
| // Keep only this notebook (and init) | ||
| notebookData.project.notebooks = notebookData.project.notebooks.filter( | ||
| (nb) => | ||
| nb.id === notebookId || | ||
| (notebookData.project.initNotebookId && nb.id === notebookData.project.initNotebookId) | ||
| ); | ||
|
|
||
| // Recompute hash | ||
| if (notebookData.metadata) { | ||
| (notebookData.metadata as Record<string, unknown>).snapshotHash = await this.computeSnapshotHash( | ||
| notebookData | ||
| ); | ||
| } | ||
|
|
||
| const newFilename = `${slug}_${projectId}_${notebookId}_${variant}.snapshot.deepnote`; | ||
| const newUri = Uri.joinPath(snapshotDir, newFilename); | ||
| const yaml = serializeDeepnoteFile(notebookData); | ||
|
|
||
| await workspace.fs.writeFile(newUri, new TextEncoder().encode(yaml)); | ||
|
|
||
| logger.debug(`[AutoSplitter] Created notebook snapshot: ${newFilename}`); | ||
| } | ||
|
|
||
| // Delete the old snapshot file (it's been replaced by per-notebook files) | ||
| try { | ||
| await workspace.fs.delete(snapshotUri); | ||
| logger.debug(`[AutoSplitter] Deleted old snapshot: ${basename}`); | ||
| } catch { | ||
| logger.warn(`[AutoSplitter] Failed to delete old snapshot: ${basename}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Extracts the variant portion from a snapshot filename. | ||
| * Old format: {slug}_{projectId}_{variant}.snapshot.deepnote | ||
| */ | ||
| private extractVariantFromSnapshotFilename(basename: string, projectId: string): string | undefined { | ||
| const suffix = '.snapshot.deepnote'; | ||
|
|
||
| if (!basename.endsWith(suffix)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const withoutSuffix = basename.slice(0, -suffix.length); | ||
| const projectIdIndex = withoutSuffix.indexOf(`_${projectId}_`); | ||
|
|
||
| if (projectIdIndex === -1) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return withoutSuffix.slice(projectIdIndex + 1 + projectId.length + 1); | ||
| } | ||
|
|
||
| /** | ||
| * Computes snapshotHash using the same algorithm as DeepnoteNotebookSerializer. | ||
| */ | ||
| private async computeSnapshotHash(project: DeepnoteFile): Promise<string> { | ||
| const contentHashes: string[] = []; | ||
|
|
||
| for (const notebook of project.project.notebooks) { | ||
| for (const block of notebook.blocks ?? []) { | ||
| if (block.contentHash) { | ||
| contentHashes.push(block.contentHash); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| contentHashes.sort(); | ||
|
|
||
| const hashInput = { | ||
| contentHashes, | ||
| environmentHash: project.environment?.hash ?? null, | ||
| integrations: (project.project.integrations ?? []) | ||
| .map((i: { id: string; name: string; type: string }) => ({ id: i.id, name: i.name, type: i.type })) | ||
| .sort((a: { id: string }, b: { id: string }) => a.id.localeCompare(b.id)), | ||
| version: project.version | ||
| }; | ||
|
|
||
| const hashData = JSON.stringify(hashInput); | ||
| const hash = await computeHash(hashData, 'SHA-256'); | ||
|
|
||
| return `sha256:${hash}`; | ||
| } | ||
|
|
||
| private getFileStem(uri: Uri): string { | ||
| const basename = uri.path.split('/').pop() ?? ''; | ||
| const dotIndex = basename.indexOf('.'); | ||
|
|
||
| return dotIndex > 0 ? basename.slice(0, dotIndex) : basename; | ||
| } | ||
|
|
||
| private slugifyNotebookName(name: string): string { | ||
| try { | ||
| return slugifyProjectName(name); | ||
| } catch { | ||
| // Fallback for names that produce empty slugs | ||
| return 'notebook'; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Reorder the private methods to match the TS file-order rule.
The private members are not alphabetized, so this new file drifts from the repo’s class layout convention.
As per coding guidelines, **/*.{ts,tsx}: Order method, fields and properties, first by accessibility and then by alphabetical order.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts` around lines 120 - 290, The
private methods in this class are out of order; reorder them to follow the repo
TS file-order rule (grouped by accessibility then alphabetically). Move the
private methods so they appear in alphabetical order: computeSnapshotHash,
extractVariantFromSnapshotFilename, getFileStem, slugifyNotebookName,
splitSingleSnapshot, splitSnapshots (use these exact function names to locate
each method) and leave their implementations unchanged.
| private async performInitialScan(): Promise<void> { | ||
| try { | ||
| await this.getDeepnoteProjectFiles(); | ||
| await this.loadAllProjectFiles(); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Don't rescan the workspace right after the initial scan.
performInitialScan() already walks every .deepnote file, then the first real root render calls getProjectGroups() and does the same discovery again. Cache the LoadedProjectFile[] from the scan and build groups from that instead.
Also applies to: 162-184, 189-190
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteTreeDataProvider.ts` around lines 148 - 150,
performInitialScan currently calls loadAllProjectFiles and then the root render
triggers getProjectGroups which repeats the same file walk; to fix, capture the
LoadedProjectFile[] result from loadAllProjectFiles in performInitialScan (e.g.
assign to a new class field like this.loadedProjectFiles) and have
getProjectGroups and any subsequent group-building logic use that cached array
instead of calling loadAllProjectFiles again. Remove duplicate calls to
loadAllProjectFiles found around getProjectGroups, and add cache
invalidation/update logic tied to file system or workspace change handlers so
the cached LoadedProjectFile[] stays in sync.
| export class TabInputNotebook { | ||
| readonly uri: vscUri.URI; | ||
| readonly notebookType: string; | ||
|
|
||
| constructor(uri: vscUri.URI, notebookType: string) { | ||
| this.uri = uri; | ||
| this.notebookType = notebookType; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Match the repo member-ordering rule here.
uri and notebookType are both public readonly properties, so they should be alphabetized within the class for consistency with the TS style rule.
As per coding guidelines, "Order method, fields and properties, first by accessibility and then by alphabetical order"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/mocks/vsc/extHostedTypes.ts` around lines 2547 - 2554, The class
TabInputNotebook declares two public readonly properties out of alphabetical
order; reorder the member declarations so public readonly notebookType appears
before public readonly uri to satisfy the repo member-ordering rule, and keep
the constructor (constructor(uri: vscUri.URI, notebookType: string)) assignments
consistent by assigning this.notebookType and this.uri after reordering the
property declarations.
… ID support - Added optional `notebookId` parameter to `executeSnapshotOutputUpdate` method to support notebook-scoped snapshots. - Updated logic to read snapshots based on the presence of `notebookId`, improving output refresh accuracy for open notebooks. - Enhanced unit tests to validate behavior when handling notebook-scoped snapshots, ensuring correct application of outputs based on matching notebook IDs.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/notebooks/deepnote/deepnoteFileChangeWatcher.ts (1)
120-139:⚠️ Potential issue | 🟡 MinorURI key mismatch:
snapshotSelfWriteUrispopulated without normalization.
onFileWrittencallback storesuri.toString()(line 123), butconsumeSelfWrite(line 173) checks usingnormalizeFileUri(uri). If a snapshot URI ever contains query/fragment, the self-write won't be detected.🛠️ Suggested fix
this.disposables.push( this.snapshotService.onFileWritten((uri) => { - const key = uri.toString(); + const key = this.normalizeFileUri(uri); this.snapshotSelfWriteUris.add(key);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts` around lines 120 - 139, The onFileWritten handler populates snapshotSelfWriteUris and snapshotSelfWriteTimers using uri.toString(), but consumeSelfWrite looks up keys via normalizeFileUri(uri), causing mismatches when URIs have queries/fragments; update the onFileWritten callback in deepnoteFileChangeWatcher.ts to compute the key with normalizeFileUri(uri) (use the same normalization for both the Set snapshotSelfWriteUris and the Map snapshotSelfWriteTimers and for clearing timers) so stored keys match consumeSelfWrite's lookup and timers are tracked/cleared consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 398-406: The save failure handling is inconsistent: when
workspace.save(notebook.uri) returns undefined the function returns, but when it
throws the catch only logs and execution continues to the success path; update
the catch for workspace.save(notebook.uri) to mirror the undefined case (i.e.,
after logging the saveError via logger.warn include an early return or rethrow)
so both failure modes prevent reaching the subsequent success log; locate this
in deepnoteFileChangeWatcher.ts around the workspace.save(...) call and the
surrounding try/catch that references notebook.uri and logger.warn.
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 131-143: The suite currently creates a temp dir with mkdtempSync
in suiteSetup and removes it in suiteTeardown, but if the test process crashes
the cleanup can be skipped; add a fail-safe removal by registering a process
exit handler that checks testFixturesDir and calls rmSync(testFixturesDir, {
recursive: true, force: true }) (and also handle SIGINT/SIGTERM) so the temp
folder is removed on abrupt termination; ensure testFixturesDir is assigned
before registering the handler and keep the existing suiteTeardown removal in
place, and reference the testFixturesDir variable and the
suiteSetup/suiteTeardown functions when making the change.
---
Outside diff comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 120-139: The onFileWritten handler populates snapshotSelfWriteUris
and snapshotSelfWriteTimers using uri.toString(), but consumeSelfWrite looks up
keys via normalizeFileUri(uri), causing mismatches when URIs have
queries/fragments; update the onFileWritten callback in
deepnoteFileChangeWatcher.ts to compute the key with normalizeFileUri(uri) (use
the same normalization for both the Set snapshotSelfWriteUris and the Map
snapshotSelfWriteTimers and for clearing timers) so stored keys match
consumeSelfWrite's lookup and timers are tracked/cleared consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: a0848e8a-4b19-412e-9a1b-f4c2c6e89f74
📒 Files selected for processing (2)
src/notebooks/deepnote/deepnoteFileChangeWatcher.tssrc/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts
| try { | ||
| const saved = await workspace.save(notebook.uri); | ||
| if (!saved) { | ||
| logger.warn(`[FileChangeWatcher] Save after sync write returned undefined: ${notebook.uri.path}`); | ||
| return; | ||
| } | ||
| } catch (saveError) { | ||
| logger.warn(`[FileChangeWatcher] Save after sync write failed: ${notebook.uri.path}`, saveError); | ||
| } |
There was a problem hiding this comment.
Inconsistent early-return on save failure.
If save() returns undefined, the method returns early. If save() throws, it logs and falls through to the success log at line 413.
🛠️ Suggested fix
} catch (saveError) {
logger.warn(`[FileChangeWatcher] Save after sync write failed: ${notebook.uri.path}`, saveError);
+ return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const saved = await workspace.save(notebook.uri); | |
| if (!saved) { | |
| logger.warn(`[FileChangeWatcher] Save after sync write returned undefined: ${notebook.uri.path}`); | |
| return; | |
| } | |
| } catch (saveError) { | |
| logger.warn(`[FileChangeWatcher] Save after sync write failed: ${notebook.uri.path}`, saveError); | |
| } | |
| try { | |
| const saved = await workspace.save(notebook.uri); | |
| if (!saved) { | |
| logger.warn(`[FileChangeWatcher] Save after sync write returned undefined: ${notebook.uri.path}`); | |
| return; | |
| } | |
| } catch (saveError) { | |
| logger.warn(`[FileChangeWatcher] Save after sync write failed: ${notebook.uri.path}`, saveError); | |
| return; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts` around lines 398 - 406,
The save failure handling is inconsistent: when workspace.save(notebook.uri)
returns undefined the function returns, but when it throws the catch only logs
and execution continues to the success path; update the catch for
workspace.save(notebook.uri) to mirror the undefined case (i.e., after logging
the saveError via logger.warn include an early return or rethrow) so both
failure modes prevent reaching the subsequent success log; locate this in
deepnoteFileChangeWatcher.ts around the workspace.save(...) call and the
surrounding try/catch that references notebook.uri and logger.warn.
| let testFixturesDir: string; | ||
|
|
||
| suiteSetup(() => { | ||
| testFixturesDir = mkdtempSync(join(tmpdir(), 'deepnote-fcw-')); | ||
| }); | ||
|
|
||
| suiteTeardown(() => { | ||
| rmSync(testFixturesDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| function testFileUri(...pathSegments: string[]): Uri { | ||
| return Uri.joinPath(Uri.file(testFixturesDir), ...pathSegments); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Temp directory with cleanup in suiteTeardown.
mkdtempSync + rmSync with recursive: true is correct. Ensure tests don't leave artifacts if the suite crashes mid-run.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts` around lines
131 - 143, The suite currently creates a temp dir with mkdtempSync in suiteSetup
and removes it in suiteTeardown, but if the test process crashes the cleanup can
be skipped; add a fail-safe removal by registering a process exit handler that
checks testFixturesDir and calls rmSync(testFixturesDir, { recursive: true,
force: true }) (and also handle SIGINT/SIGTERM) so the temp folder is removed on
abrupt termination; ensure testFixturesDir is assigned before registering the
handler and keep the existing suiteTeardown removal in place, and reference the
testFixturesDir variable and the suiteSetup/suiteTeardown functions when making
the change.
- Enhanced `DeepnoteNotebookManager` to manage projects using a (projectId, notebookId) pair, allowing for distinct storage of projects across multiple notebooks. - Updated methods `getOriginalProject`, `storeOriginalProject`, and `updateOriginalProject` to accept an optional notebook ID, improving project data handling for split files. - Refactored related classes and tests to ensure compatibility with the new notebook ID structure, enhancing overall functionality and reliability. - Improved snapshot handling in `SnapshotService` and `DeepnoteSerializer` to utilize notebook IDs for accurate project retrieval and serialization.
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (3)
src/notebooks/deepnote/deepnoteFileChangeWatcher.ts (1)
398-406:⚠️ Potential issue | 🟡 MinorReturn after
workspace.save()throws.The
false/undefinedpath returns, but the exception path just logs and then falls through to the success log at Line 413.Patch
} catch (saveError) { logger.warn(`[FileChangeWatcher] Save after sync write failed: ${notebook.uri.path}`, saveError); + return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts` around lines 398 - 406, The catch for the workspace.save(notebook.uri) call currently only logs the error (logger.warn) and then execution falls through to the success log; update the catch to stop further processing by returning (or otherwise exiting) after logging the saveError so the code does not incorrectly log a successful save for notebook.uri.path. Ensure you modify the catch block that wraps workspace.save(notebook.uri) (the logger.warn call that references notebook.uri.path and saveError) to return immediately after logging.src/notebooks/deepnote/deepnoteExplorerView.ts (2)
77-85:⚠️ Potential issue | 🟠 MajorCreating a notebook still falls back to the file’s default notebook.
This path appends a second notebook into the same
.deepnotefile and then reopens onlyfileUri. Since deserialization now resolves a default notebook, the editor will usually land on the preexisting/default notebook instead of the one that was just created. Either create a new per-notebook file here, or carry the target notebook through the reopen path.Also applies to: 493-510
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteExplorerView.ts` around lines 77 - 85, The current flow appends newNotebook to projectData.project.notebooks then calls saveProjectAndOpenNotebook(fileUri), which reopens the .deepnote file and usually selects the default notebook instead of the newly created one; fix by carrying the target notebook through the reopen path: change saveProjectAndOpenNotebook to accept an identifier/ordinal for the notebook (e.g., newNotebook.id or the newNotebook object) and use that to select/open the specific notebook after deserialization, then update this call site to pass newNotebook (or newNotebook.id) instead of only fileUri; alternatively, if you prefer per-notebook files, create a new .deepnote file for newNotebook instead of appending to projectData.project.notebooks and open that new file—adjust saveProjectAndOpenNotebook and its callers accordingly (symbols: projectData.project.notebooks, newNotebook, saveProjectAndOpenNotebook, fileUri).
253-271:⚠️ Potential issue | 🟠 MajorDuplicate reopens the original/default notebook, not the copy.
newNotebookis appended, but the reopen step still targets onlyfileUri. In a multi-notebook file that resolves back to the default notebook, so the duplicated notebook is not what the user sees after the command completes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteExplorerView.ts` around lines 253 - 271, The code writes the updated file but reopens using fileUri, which opens the default notebook instead of the duplicated one; update the reopen step to open the specific notebook entry for newNotebook by constructing a notebook-specific URI (e.g., derive a fragment/identifier from newNotebook.id or its index in projectData.project.notebooks) and pass that URI to workspace.openNotebookDocument and window.showNotebookDocument so the duplicated notebook (newNotebook) is opened rather than the file's default notebook.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 446-462: The private method disposeOutputListeners is placed
immediately before the public dispose method, violating the rule to order class
members first by accessibility then alphabetically; move disposeOutputListeners
so that all public members (including dispose) appear before private members, or
otherwise reorder members so that public dispose() comes before private
disposeOutputListeners and ensure disposablesByFile usage remains correct;
update the class member sequence so accessibility groups (public then private)
and alphabetical ordering within each group are respected for dispose() and
disposeOutputListeners.
In `@src/notebooks/deepnote/deepnoteNotebookManager.ts`:
- Around line 35-43: The current logic in the notebook lookup returns the first
entry from notebookMap even when a caller requested a specific notebookId and it
wasn't found, which can return the wrong notebook; change the behavior in the
function handling notebook lookups (the block referencing notebookId and
notebookMap) so that when notebookId is provided and notebookMap.get(notebookId)
returns undefined you return undefined immediately, and only use the
notebookMap.values().next().value fallback when notebookId was not provided
(i.e., the caller intentionally omitted it).
In `@src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts`:
- Around line 35-40: The tests store using
manager.storeOriginalProject('project-123', 'notebook-1', mockProject') but read
back with manager.getOriginalProject('project-123'), which doesn't verify
notebook-aware behavior; update the assertions in the new cache-contract tests
so they call manager.getOriginalProject('project-123', 'notebook-1') to read
with the same notebookId (do the same for the second case at the other test
block), and keep one separate test that asserts the fallback behavior using
getOriginalProject('project-123') only once.
In `@src/notebooks/deepnote/deepnoteTreeItem.ts`:
- Around line 53-58: The ProjectGroup/ProjectFile visual-field formatting logic
is duplicated between the DeepnoteTreeItem constructor and updateVisualFields();
extract shared helpers (e.g., formatProjectGroupVisuals(projectGroupData) and
formatProjectFileVisuals(projectFileData)) that return label, tooltip,
description, and icon info, and call those helpers from both the constructor and
updateVisualFields(); update usages for DeepnoteTreeItemType.ProjectGroup and
DeepnoteTreeItemType.ProjectFile so the rendering rules live in one place and
both code paths consume the same helper functions.
In `@src/notebooks/deepnote/snapshots/snapshotService.ts`:
- Around line 773-779: The code currently calls findProjectUriFromId(projectId)
which can return a sibling file's URI; instead derive the snapshot location from
the current notebook's URI (the `notebook` variable) so snapshots are written
for the correct file. Replace the findProjectUriFromId(projectId) call with
logic that uses `notebook.uri` (or a helper like
findProjectUriFromUri(notebook.uri) if available) as the source of truth for
`projectUri`, keep the same early-return and logger.warn behavior but reference
the notebook URI in the warning.
- Around line 415-418: The notebook-scoped glob construction (latestGlob)
currently omits legacy project-scoped snapshots when notebookId is present;
change the logic in snapshotService.ts so that when notebookId is provided you
first build the notebook-scoped pattern
(`*_{projectId}_{notebookId}_latest.snapshot.deepnote`) and if that yields no
results fall back to the legacy project-scoped pattern
(`*_{projectId}_latest.snapshot.deepnote`); apply the same change to the other
similar block around the code referenced at 443-445 so both the "latest" and the
other snapshot-glob checks try the notebook-scoped pattern first and then
fallback to the legacy project-scoped pattern when no matches are found.
---
Duplicate comments:
In `@src/notebooks/deepnote/deepnoteExplorerView.ts`:
- Around line 77-85: The current flow appends newNotebook to
projectData.project.notebooks then calls saveProjectAndOpenNotebook(fileUri),
which reopens the .deepnote file and usually selects the default notebook
instead of the newly created one; fix by carrying the target notebook through
the reopen path: change saveProjectAndOpenNotebook to accept an
identifier/ordinal for the notebook (e.g., newNotebook.id or the newNotebook
object) and use that to select/open the specific notebook after deserialization,
then update this call site to pass newNotebook (or newNotebook.id) instead of
only fileUri; alternatively, if you prefer per-notebook files, create a new
.deepnote file for newNotebook instead of appending to
projectData.project.notebooks and open that new file—adjust
saveProjectAndOpenNotebook and its callers accordingly (symbols:
projectData.project.notebooks, newNotebook, saveProjectAndOpenNotebook,
fileUri).
- Around line 253-271: The code writes the updated file but reopens using
fileUri, which opens the default notebook instead of the duplicated one; update
the reopen step to open the specific notebook entry for newNotebook by
constructing a notebook-specific URI (e.g., derive a fragment/identifier from
newNotebook.id or its index in projectData.project.notebooks) and pass that URI
to workspace.openNotebookDocument and window.showNotebookDocument so the
duplicated notebook (newNotebook) is opened rather than the file's default
notebook.
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 398-406: The catch for the workspace.save(notebook.uri) call
currently only logs the error (logger.warn) and then execution falls through to
the success log; update the catch to stop further processing by returning (or
otherwise exiting) after logging the saveError so the code does not incorrectly
log a successful save for notebook.uri.path. Ensure you modify the catch block
that wraps workspace.save(notebook.uri) (the logger.warn call that references
notebook.uri.path and saveError) to return immediately after logging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 95692a9c-a6cd-4c59-91ec-1fc63e37e257
📒 Files selected for processing (13)
src/kernels/deepnote/deepnoteServerStarter.node.tssrc/notebooks/deepnote/deepnoteExplorerView.tssrc/notebooks/deepnote/deepnoteExplorerView.unit.test.tssrc/notebooks/deepnote/deepnoteFileChangeWatcher.tssrc/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.tssrc/notebooks/deepnote/deepnoteKernelAutoSelector.node.tssrc/notebooks/deepnote/deepnoteNotebookManager.tssrc/notebooks/deepnote/deepnoteNotebookManager.unit.test.tssrc/notebooks/deepnote/deepnoteSerializer.tssrc/notebooks/deepnote/deepnoteSerializer.unit.test.tssrc/notebooks/deepnote/deepnoteTreeItem.tssrc/notebooks/deepnote/snapshots/snapshotService.tssrc/notebooks/types.ts
| /** | ||
| * Dispose all output listeners registered for a given file key. | ||
| * Per-listener try/catch ensures one failure doesn't prevent others from being disposed. | ||
| */ | ||
| private disposeOutputListeners(fileKey: string): void { | ||
| const disposables = this.disposablesByFile.get(fileKey) ?? []; | ||
| for (const d of disposables) { | ||
| try { | ||
| d.dispose(); | ||
| } catch (ex) { | ||
| logger.warn(`Error disposing output listener for ${fileKey}`, ex); | ||
| } | ||
| } | ||
| this.disposablesByFile.delete(fileKey); | ||
| } | ||
|
|
||
| public async dispose(): Promise<void> { |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Reorder members to match required class ordering.
private disposeOutputListeners is introduced immediately before public dispose(), which breaks the required accessibility/alphabetical ordering in this section. Please reorder this block to align with the class member ordering rule. As per coding guidelines, "Order method, fields and properties, first by accessibility and then by alphabetical order".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 446 - 462,
The private method disposeOutputListeners is placed immediately before the
public dispose method, violating the rule to order class members first by
accessibility then alphabetically; move disposeOutputListeners so that all
public members (including dispose) appear before private members, or otherwise
reorder members so that public dispose() comes before private
disposeOutputListeners and ensure disposablesByFile usage remains correct;
update the class member sequence so accessibility groups (public then private)
and alphabetical ordering within each group are respected for dispose() and
disposeOutputListeners.
| if (notebookId !== undefined) { | ||
| const exactMatch = notebookMap.get(notebookId); | ||
|
|
||
| if (exactMatch) { | ||
| return exactMatch; | ||
| } | ||
| } | ||
|
|
||
| return notebookMap.values().next().value; |
There was a problem hiding this comment.
Don’t hide an exact notebook-cache miss behind an arbitrary project entry.
After the split-file migration, cached entries under one projectId are no longer interchangeable: each can carry a different project.notebooks subset. If a caller asked for a specific notebookId and it is missing, returning the first cached value can hand back a sibling notebook’s file and make later save/restore logic act on the wrong project. Return undefined on an exact miss; keep the fallback only for calls that omitted notebookId on purpose.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteNotebookManager.ts` around lines 35 - 43, The
current logic in the notebook lookup returns the first entry from notebookMap
even when a caller requested a specific notebookId and it wasn't found, which
can return the wrong notebook; change the behavior in the function handling
notebook lookups (the block referencing notebookId and notebookMap) so that when
notebookId is provided and notebookMap.get(notebookId) returns undefined you
return undefined immediately, and only use the notebookMap.values().next().value
fallback when notebookId was not provided (i.e., the caller intentionally
omitted it).
| test('should return original project after storing', () => { | ||
| manager.storeOriginalProject('project-123', mockProject, 'notebook-456'); | ||
| manager.storeOriginalProject('project-123', 'notebook-1', mockProject); | ||
|
|
||
| const result = manager.getOriginalProject('project-123'); | ||
|
|
||
| assert.deepStrictEqual(result, mockProject); |
There was a problem hiding this comment.
Read back with the notebook ID in these new cache-contract tests.
These cases store under 'notebook-1' and then assert via getOriginalProject('project-123'). That still passes if the manager ignores notebookId, which is the main behavior this PR is introducing. Keep one explicit fallback test, but make the new contract tests read back with the same notebook ID.
♻️ Minimal adjustment
-const result = manager.getOriginalProject('project-123');
+const result = manager.getOriginalProject('project-123', 'notebook-1');Also applies to: 45-50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts` around lines 35
- 40, The tests store using manager.storeOriginalProject('project-123',
'notebook-1', mockProject') but read back with
manager.getOriginalProject('project-123'), which doesn't verify notebook-aware
behavior; update the assertions in the new cache-contract tests so they call
manager.getOriginalProject('project-123', 'notebook-1') to read with the same
notebookId (do the same for the second case at the other test block), and keep
one separate test that asserts the fallback behavior using
getOriginalProject('project-123') only once.
| } else if (this.type === DeepnoteTreeItemType.ProjectGroup) { | ||
| const groupData = this.data as ProjectGroupData; | ||
| this.label = groupData.projectName || 'Untitled Project'; | ||
| this.tooltip = `Deepnote Project: ${groupData.projectName}\n${groupData.files.length} file(s)`; | ||
| this.description = `${groupData.files.length} file${groupData.files.length !== 1 ? 's' : ''}`; | ||
| this.iconPath = new ThemeIcon('notebook'); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract the repeated visual-field formatting into helpers.
The constructor and updateVisualFields() now duplicate the same ProjectGroup and ProjectFile rendering rules. That is easy to drift the next time labels, tooltips, or counts change.
As per coding guidelines "Extract duplicate logic into helper methods to prevent drift following DRY principle".
Also applies to: 77-92, 132-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteTreeItem.ts` around lines 53 - 58, The
ProjectGroup/ProjectFile visual-field formatting logic is duplicated between the
DeepnoteTreeItem constructor and updateVisualFields(); extract shared helpers
(e.g., formatProjectGroupVisuals(projectGroupData) and
formatProjectFileVisuals(projectFileData)) that return label, tooltip,
description, and icon info, and call those helpers from both the constructor and
updateVisualFields(); update usages for DeepnoteTreeItemType.ProjectGroup and
DeepnoteTreeItemType.ProjectFile so the rendering rules live in one place and
both code paths consume the same helper functions.
| // 1. Try to find a 'latest' snapshot file (new format with notebookId first, then legacy) | ||
| const latestGlob = notebookId | ||
| ? `**/snapshots/*_${projectId}_${notebookId}_latest.snapshot.deepnote` | ||
| : `**/snapshots/*_${projectId}_latest.snapshot.deepnote`; |
There was a problem hiding this comment.
Notebook-scoped reads dropped the legacy snapshot fallback.
When notebookId is present, these globs only search *_${projectId}_${notebookId}_.... Older *_${projectId}_... snapshots will never be found, so pre-migration outputs disappear until a new notebook-scoped snapshot is written. Please try the notebook-scoped pattern first and then fall back to the legacy project-scoped one.
Also applies to: 443-445
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/snapshots/snapshotService.ts` around lines 415 - 418,
The notebook-scoped glob construction (latestGlob) currently omits legacy
project-scoped snapshots when notebookId is present; change the logic in
snapshotService.ts so that when notebookId is provided you first build the
notebook-scoped pattern (`*_{projectId}_{notebookId}_latest.snapshot.deepnote`)
and if that yields no results fall back to the legacy project-scoped pattern
(`*_{projectId}_latest.snapshot.deepnote`); apply the same change to the other
similar block around the code referenced at 443-445 so both the "latest" and the
other snapshot-glob checks try the notebook-scoped pattern first and then
fallback to the legacy project-scoped pattern when no matches are found.
| const projectUri = this.findProjectUriFromId(projectId); | ||
|
|
||
| if (!notebookId) { | ||
| logger.warn(`[Snapshot] No notebook ID in notebook metadata`); | ||
| if (!projectUri) { | ||
| logger.warn(`[Snapshot] Could not find project URI for ${projectId}`); | ||
|
|
||
| return; | ||
| } |
There was a problem hiding this comment.
Resolve the snapshot location from the current notebook, not just projectId.
After the split-file migration, several open .deepnote files can share one projectId. findProjectUriFromId(projectId) can therefore pick a sibling file, which writes this notebook’s snapshots into the wrong directory. You already have notebook here, so use its URI as the source of truth.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/snapshots/snapshotService.ts` around lines 773 - 779,
The code currently calls findProjectUriFromId(projectId) which can return a
sibling file's URI; instead derive the snapshot location from the current
notebook's URI (the `notebook` variable) so snapshots are written for the
correct file. Replace the findProjectUriFromId(projectId) call with logic that
uses `notebook.uri` (or a helper like findProjectUriFromUri(notebook.uri) if
available) as the source of truth for `projectUri`, keep the same early-return
and logger.warn behavior but reference the notebook URI in the warning.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts (1)
787-799:⚠️ Potential issue | 🟠 MajorThis test never isolates
noSnapshotWatcher.
noSnapshotWatcheris created here, but the fired event (snapshotOnDidChange) and the asserted counters still belong to the already-activesnapshotWatcher. That means this can stay green without proving the new watcher ignores snapshot events. GivenoSnapshotWatcherits ownFileSystemWatchermock/event emitters, then fire those and assert no read/apply happens.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts` around lines 787 - 799, The test incorrectly reuses the existing snapshot event stream so noSnapshotWatcher isn't isolated; create a separate mocked FileSystemWatcher and event emitter for the new DeepnoteFileChangeWatcher instance (noSnapshotWatcher) instead of reusing snapshotOnDidChange or snapshotWatcher, wire that mock into noSnapshotWatcher during construction/activation, fire the new watcher’s change event (the new event emitter) and then await debounceWaitMs, and finally assert readSnapshotCallCount and any applySnapshot counts remain unchanged to prove the new watcher ignored snapshot events; ensure workspace.notebookDocuments is still mocked to [] when firing the new event.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 295-296: Replace scattered per-test numeric timeouts (e.g., 8000,
10000, 12000, 15000, 20000) with named constants declared near the top of the
module and reuse them in each test's this.timeout(...) call; specifically add
constants like TEST_TIMEOUT_SHORT, TEST_TIMEOUT_MEDIUM, TEST_TIMEOUT_LONG (or
similar) and update the tests referenced by their unique names such as the
"saving a non-deepnote notebook should not suppress FS change events" test and
the other tests noted in the comment so that each this.timeout(...) uses the
appropriate named constant instead of a literal; keep constants alongside the
existing timing constants block for easy CI tuning.
- Around line 1618-1630: The test is falsely passing because a suite-level
one-arg stub for mockSnapshotService.readSnapshot still exists; call
reset(mockSnapshotService) (not resetCalls) before re-stubbing so previous stubs
are removed, then stub readSnapshot explicitly with two args via
when(mockSnapshotService.readSnapshot(anything(), anything())).thenCall(...) and
increment/read readSnapshotCallCount as done, and finally verify the exact call
happened with verify(mockSnapshotService.readSnapshot('project-1',
scopedNotebookId)).once(); apply the same reset+explicit two-arg stubbing and
exact-args verification for the other occurrence around the 1655–1660 region.
---
Outside diff comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 787-799: The test incorrectly reuses the existing snapshot event
stream so noSnapshotWatcher isn't isolated; create a separate mocked
FileSystemWatcher and event emitter for the new DeepnoteFileChangeWatcher
instance (noSnapshotWatcher) instead of reusing snapshotOnDidChange or
snapshotWatcher, wire that mock into noSnapshotWatcher during
construction/activation, fire the new watcher’s change event (the new event
emitter) and then await debounceWaitMs, and finally assert readSnapshotCallCount
and any applySnapshot counts remain unchanged to prove the new watcher ignored
snapshot events; ensure workspace.notebookDocuments is still mocked to [] when
firing the new event.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 162ff883-0820-4b85-ba7f-22126c875efd
📒 Files selected for processing (1)
src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts
| test('saving a non-deepnote notebook should not suppress FS change events', async function () { | ||
| this.timeout(8000); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Extract the per-test timeout literals into named constants.
The new 8_000/10_000/12_000/15_000/20_000 budgets are now scattered through the suite, which makes CI tuning harder than it needs to be. Hoist them next to the other timing constants and reuse them.
As per coding guidelines, "Extract magic numbers (retry counts, delays, timeouts) as named constants near the top of the module".
Also applies to: 319-320, 510-512, 566-567, 1898-1899, 1972-1973, 2041-2042, 2543-2544
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts` around lines
295 - 296, Replace scattered per-test numeric timeouts (e.g., 8000, 10000,
12000, 15000, 20000) with named constants declared near the top of the module
and reuse them in each test's this.timeout(...) call; specifically add constants
like TEST_TIMEOUT_SHORT, TEST_TIMEOUT_MEDIUM, TEST_TIMEOUT_LONG (or similar) and
update the tests referenced by their unique names such as the "saving a
non-deepnote notebook should not suppress FS change events" test and the other
tests noted in the comment so that each this.timeout(...) uses the appropriate
named constant instead of a literal; keep constants alongside the existing
timing constants block for easy CI tuning.
| resetCalls(mockSnapshotService); | ||
| when(mockSnapshotService.isSnapshotsEnabled()).thenReturn(true); | ||
| when(mockSnapshotService.readSnapshot(anything(), anything())).thenCall((pid: string, nid?: string) => { | ||
| readSnapshotCallCount++; | ||
| if (typeof nid === 'undefined') { | ||
| return Promise.resolve(undefined); | ||
| } | ||
| if (pid === 'project-1' && nid === scopedNotebookId) { | ||
| return Promise.resolve(snapshotOutputs); | ||
| } | ||
|
|
||
| return Promise.resolve(undefined); | ||
| }); |
There was a problem hiding this comment.
This regression check is masked by the leftover 1-arg readSnapshot stub.
resetCalls(mockSnapshotService) clears invocations, not stubs. The suite-level readSnapshot(anything()) setup is still active, so a regression back to readSnapshot('project-1') can still return snapshotOutputs and satisfy this test because the notebook also uses block-1. Reset the mock here and assert the exact (projectId, notebookId) pair that was read.
Patch sketch
- resetCalls(mockSnapshotService);
+ reset(mockSnapshotService);
when(mockSnapshotService.isSnapshotsEnabled()).thenReturn(true);
+ const readArgs: Array<{ projectId: string; notebookId?: string }> = [];
when(mockSnapshotService.readSnapshot(anything(), anything())).thenCall((pid: string, nid?: string) => {
readSnapshotCallCount++;
+ readArgs.push({ projectId: pid, notebookId: nid });
if (typeof nid === 'undefined') {
return Promise.resolve(undefined);
}
if (pid === 'project-1' && nid === scopedNotebookId) {
return Promise.resolve(snapshotOutputs);
}
return Promise.resolve(undefined);
});
…
- assert.isAtLeast(readSnapshotCallCount, 1, 'readSnapshot should be called');
+ assert.deepStrictEqual(readArgs, [{ projectId: 'project-1', notebookId: scopedNotebookId }]);Also applies to: 1655-1660
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts` around lines
1618 - 1630, The test is falsely passing because a suite-level one-arg stub for
mockSnapshotService.readSnapshot still exists; call reset(mockSnapshotService)
(not resetCalls) before re-stubbing so previous stubs are removed, then stub
readSnapshot explicitly with two args via
when(mockSnapshotService.readSnapshot(anything(), anything())).thenCall(...) and
increment/read readSnapshotCallCount as done, and finally verify the exact call
happened with verify(mockSnapshotService.readSnapshot('project-1',
scopedNotebookId)).once(); apply the same reset+explicit two-arg stubbing and
exact-args verification for the other occurrence around the 1655–1660 region.
…tory methods - Added `deepnoteNotebookFileFactory` to encapsulate logic for creating single notebook files and generating sibling file URIs. - Updated `DeepnoteAutoSplitter` and `DeepnoteExplorerView` to utilize the new factory methods, improving code organization and readability. - Enhanced snapshot hash computation and slugification logic for notebook names, ensuring consistent file naming and metadata handling. - Introduced unit tests for the new factory methods to validate functionality and maintain code quality.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/notebooks/deepnote/deepnoteExplorerView.ts (2)
288-289:⚠️ Potential issue | 🟠 MajorDon't reopen the base file after duplication.
This writes the copy into the source
.deepnoteand then reopensfileUriagain, so the UI lands back on the source file instead of the duplicated notebook. In the single-notebook flow, duplication should open the duplicated notebook explicitly, or better, create/open a sibling notebook file the same waycreateAndAddNotebookToProject()does.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteExplorerView.ts` around lines 288 - 289, The code currently reopens the original fileUri after duplication which returns the UI to the source instead of the new copy; change the logic to open the duplicated notebook's URI (not fileUri) — locate where workspace.openNotebookDocument(fileUri) is called and replace it to open the new duplicatedFileUri (or use the same sibling-creation flow as createAndAddNotebookToProject()), ensuring the duplicated notebook is created/added as a sibling and then passed to workspace.openNotebookDocument and shown in the editor so the UI lands on the new copy.
559-560:⚠️ Potential issue | 🟠 MajorPass the notebook target through this open path.
context.notebookIdis ignored here, so notebook nodes under the same file become indistinguishable and all reopen the same document.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteExplorerView.ts` around lines 559 - 560, The code opens the file URI without including the notebook identifier so different notebook nodes for the same file collide; include context.notebookId when building the URI passed to workspace.openNotebookDocument (e.g. use Uri.file(context.filePath).with({ query: `notebookId=${context.notebookId}` }) or otherwise encode context.notebookId into the Uri) and then call workspace.openNotebookDocument with that enriched URI so the correct notebook target (context.notebookId) is preserved; update the usage around Uri.file and workspace.openNotebookDocument in deepnoteExplorerView.ts to reference context.notebookId.src/notebooks/deepnote/deepnoteAutoSplitter.ts (2)
51-63:⚠️ Potential issue | 🔴 CriticalMake the split loop collision-safe and failure-safe.
Slug-only filenames still collide for duplicate names or the
'notebook'fallback, and one failedwriteFileaborts the loop after earlier siblings were already written. That can overwrite notebooks or leave the project half-split on disk.Proposed direction
- for (const notebook of extraNotebooks) { - const notebookSlug = slugifyNotebookNameOrFallback(notebook.name); - const newFileName = `${originalStem}_${notebookSlug}.deepnote`; - const newFileUri = Uri.joinPath(parentDir, newFileName); - - const newProject = await buildSingleNotebookFile(deepnoteFile, notebook); - - const yaml = serializeDeepnoteFile(newProject); - await workspace.fs.writeFile(newFileUri, new TextEncoder().encode(yaml)); - - newFiles.push(newFileUri); - - logger.info(`[AutoSplitter] Created ${newFileName} for notebook "${notebook.name}"`); + for (const notebook of extraNotebooks) { + try { + const newFileUri = await buildSiblingNotebookFileUri(fileUri, notebook.name, async (candidate) => { + try { + await workspace.fs.stat(candidate); + return true; + } catch { + return false; + } + }); + const newFileName = newFileUri.path.split('/').pop() ?? newFileUri.path; + const newProject = await buildSingleNotebookFile(deepnoteFile, notebook); + const yaml = serializeDeepnoteFile(newProject); + + await workspace.fs.writeFile(newFileUri, new TextEncoder().encode(yaml)); + newFiles.push(newFileUri); + logger.info(`[AutoSplitter] Created ${newFileName} for notebook "${notebook.name}"`); + } catch (error) { + logger.warn(`[AutoSplitter] Failed to split notebook "${notebook.name}"`, error); + } }As per coding guidelines,
Extract duplicate logic into helper methods to prevent drift following DRY principleandUse per-iteration error handling in loops - wrap each iteration in try/catch so one failure doesn't stop the rest.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts` around lines 51 - 63, The split loop must be made collision-safe and failure-safe: extract the filename-generation logic used around slugifyNotebookNameOrFallback into a helper (e.g., generateUniqueFileUri(parentDir, originalStem, notebook.name)) that checks for existing URIs and appends a unique suffix/counter if needed; wrap each iteration in a try/catch so a failing workspace.fs.writeFile for one notebook doesn't abort the loop and still records/logs the error via logger.error; make writes atomic by writing to a temp URI (e.g., newFileUri + ".tmp") and then renaming/moving to newFileUri after successful write; ensure newFiles is only pushed after the final successful rename and use buildSingleNotebookFile/serializeDeepnoteFile as before.
143-146:⚠️ Potential issue | 🟠 MajorSkip snapshots that already use notebook-scoped names.
snapshotGlobmatches both legacy and new snapshot filenames, butextractVariantFromSnapshotFilename()only parses the legacy form. For an already-migrated file, this path treats<notebookId>_<variant>as the variant, writes a doubly suffixed filename, and then deletes the valid source snapshot.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts` around lines 143 - 146, The loop over allSnapshotFiles is treating already-migrated snapshot filenames as legacy and re-suffixed; update the logic in the iteration (or inside splitSingleSnapshot) to detect and skip snapshot files that already use the notebook-scoped naming scheme before attempting to parse or rewrite them. Specifically, add a guard that recognizes the new filename form (the notebook-scoped pattern your migration uses) — e.g., check the snapshot filename against the notebook-scoped regex or detect that extractVariantFromSnapshotFilename would mis-interpret a notebookId_prefix — and return/continue early so splitSingleSnapshot is not called and the original snapshot is not deleted. Ensure the check references the same helper(s) (extractVariantFromSnapshotFilename, splitSingleSnapshot) and preserves behavior for legacy filenames.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts`:
- Around line 70-75: The metadata.modifiedAt field on deepnoteFile should be
refreshed before rewriting the source file: in the block that sets
(deepnoteFile.metadata as Record<string, unknown>).snapshotHash = await
computeSnapshotHash(deepnoteFile), also update (deepnoteFile.metadata as
Record<string, unknown>).modifiedAt to the current timestamp (e.g. new
Date().toISOString() or Date.now()) so serializeDeepnoteFile(deepnoteFile) and
the subsequent workspace.fs.writeFile(fileUri, new
TextEncoder().encode(updatedYaml)) persist a fresh modification time consistent
with newly created sibling files.
In `@src/notebooks/deepnote/deepnoteExplorerView.ts`:
- Around line 547-549: The public method refreshTree() is currently located
after private helpers; move the refreshTree() declaration into the class's
public member block so it sits with other public methods and is alphabetized
among them (ensure its signature public refreshTree(): void {
this.treeDataProvider.refresh(); } is unchanged and references
treeDataProvider.refresh()).
---
Duplicate comments:
In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts`:
- Around line 51-63: The split loop must be made collision-safe and
failure-safe: extract the filename-generation logic used around
slugifyNotebookNameOrFallback into a helper (e.g.,
generateUniqueFileUri(parentDir, originalStem, notebook.name)) that checks for
existing URIs and appends a unique suffix/counter if needed; wrap each iteration
in a try/catch so a failing workspace.fs.writeFile for one notebook doesn't
abort the loop and still records/logs the error via logger.error; make writes
atomic by writing to a temp URI (e.g., newFileUri + ".tmp") and then
renaming/moving to newFileUri after successful write; ensure newFiles is only
pushed after the final successful rename and use
buildSingleNotebookFile/serializeDeepnoteFile as before.
- Around line 143-146: The loop over allSnapshotFiles is treating
already-migrated snapshot filenames as legacy and re-suffixed; update the logic
in the iteration (or inside splitSingleSnapshot) to detect and skip snapshot
files that already use the notebook-scoped naming scheme before attempting to
parse or rewrite them. Specifically, add a guard that recognizes the new
filename form (the notebook-scoped pattern your migration uses) — e.g., check
the snapshot filename against the notebook-scoped regex or detect that
extractVariantFromSnapshotFilename would mis-interpret a notebookId_prefix — and
return/continue early so splitSingleSnapshot is not called and the original
snapshot is not deleted. Ensure the check references the same helper(s)
(extractVariantFromSnapshotFilename, splitSingleSnapshot) and preserves behavior
for legacy filenames.
In `@src/notebooks/deepnote/deepnoteExplorerView.ts`:
- Around line 288-289: The code currently reopens the original fileUri after
duplication which returns the UI to the source instead of the new copy; change
the logic to open the duplicated notebook's URI (not fileUri) — locate where
workspace.openNotebookDocument(fileUri) is called and replace it to open the new
duplicatedFileUri (or use the same sibling-creation flow as
createAndAddNotebookToProject()), ensuring the duplicated notebook is
created/added as a sibling and then passed to workspace.openNotebookDocument and
shown in the editor so the UI lands on the new copy.
- Around line 559-560: The code opens the file URI without including the
notebook identifier so different notebook nodes for the same file collide;
include context.notebookId when building the URI passed to
workspace.openNotebookDocument (e.g. use Uri.file(context.filePath).with({
query: `notebookId=${context.notebookId}` }) or otherwise encode
context.notebookId into the Uri) and then call workspace.openNotebookDocument
with that enriched URI so the correct notebook target (context.notebookId) is
preserved; update the usage around Uri.file and workspace.openNotebookDocument
in deepnoteExplorerView.ts to reference context.notebookId.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 018b4118-b36b-4f59-8ea2-55f48ebf9c0c
📒 Files selected for processing (5)
src/notebooks/deepnote/deepnoteAutoSplitter.tssrc/notebooks/deepnote/deepnoteExplorerView.tssrc/notebooks/deepnote/deepnoteExplorerView.unit.test.tssrc/notebooks/deepnote/deepnoteNotebookFileFactory.tssrc/notebooks/deepnote/deepnoteNotebookFileFactory.unit.test.ts
| if (deepnoteFile.metadata) { | ||
| (deepnoteFile.metadata as Record<string, unknown>).snapshotHash = await computeSnapshotHash(deepnoteFile); | ||
| } | ||
|
|
||
| const updatedYaml = serializeDeepnoteFile(deepnoteFile); | ||
| await workspace.fs.writeFile(fileUri, new TextEncoder().encode(updatedYaml)); |
There was a problem hiding this comment.
Refresh metadata.modifiedAt before rewriting the source file.
The splitter mutates and overwrites the original project here, but only snapshotHash changes. That leaves the migrated source file with stale metadata while the newly created sibling files get a fresh timestamp.
Proposed fix
- if (deepnoteFile.metadata) {
- (deepnoteFile.metadata as Record<string, unknown>).snapshotHash = await computeSnapshotHash(deepnoteFile);
- }
+ if (!deepnoteFile.metadata) {
+ deepnoteFile.metadata = { createdAt: new Date().toISOString() };
+ }
+ deepnoteFile.metadata.modifiedAt = new Date().toISOString();
+ (deepnoteFile.metadata as Record<string, unknown>).snapshotHash = await computeSnapshotHash(deepnoteFile);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (deepnoteFile.metadata) { | |
| (deepnoteFile.metadata as Record<string, unknown>).snapshotHash = await computeSnapshotHash(deepnoteFile); | |
| } | |
| const updatedYaml = serializeDeepnoteFile(deepnoteFile); | |
| await workspace.fs.writeFile(fileUri, new TextEncoder().encode(updatedYaml)); | |
| if (!deepnoteFile.metadata) { | |
| deepnoteFile.metadata = { createdAt: new Date().toISOString() }; | |
| } | |
| deepnoteFile.metadata.modifiedAt = new Date().toISOString(); | |
| (deepnoteFile.metadata as Record<string, unknown>).snapshotHash = await computeSnapshotHash(deepnoteFile); | |
| const updatedYaml = serializeDeepnoteFile(deepnoteFile); | |
| await workspace.fs.writeFile(fileUri, new TextEncoder().encode(updatedYaml)); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteAutoSplitter.ts` around lines 70 - 75, The
metadata.modifiedAt field on deepnoteFile should be refreshed before rewriting
the source file: in the block that sets (deepnoteFile.metadata as Record<string,
unknown>).snapshotHash = await computeSnapshotHash(deepnoteFile), also update
(deepnoteFile.metadata as Record<string, unknown>).modifiedAt to the current
timestamp (e.g. new Date().toISOString() or Date.now()) so
serializeDeepnoteFile(deepnoteFile) and the subsequent
workspace.fs.writeFile(fileUri, new TextEncoder().encode(updatedYaml)) persist a
fresh modification time consistent with newly created sibling files.
| public refreshTree(): void { | ||
| this.treeDataProvider.refresh(); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Keep refreshTree() in the public member block.
This new public method is placed after private helpers, which drifts from the repo’s member-order rule. Move it alongside the other public methods.
As per coding guidelines, **/*.{ts,tsx}: Order method, fields and properties, first by accessibility and then by alphabetical order.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/notebooks/deepnote/deepnoteExplorerView.ts` around lines 547 - 549, The
public method refreshTree() is currently located after private helpers; move the
refreshTree() declaration into the class's public member block so it sits with
other public methods and is alphabetized among them (ensure its signature public
refreshTree(): void { this.treeDataProvider.refresh(); } is unchanged and
references treeDataProvider.refresh()).
Summary by CodeRabbit
New Features
Improvements