Skip to content

feat(deepnote-file): Migration to single notebook deepnote file#381

Draft
tkislan wants to merge 45 commits intomainfrom
tk/single-notebook-deepnote-file
Draft

feat(deepnote-file): Migration to single notebook deepnote file#381
tkislan wants to merge 45 commits intomainfrom
tk/single-notebook-deepnote-file

Conversation

@tkislan
Copy link
Copy Markdown
Contributor

@tkislan tkislan commented Apr 13, 2026

Summary by CodeRabbit

  • New Features

    • Status bar item and command to copy active Deepnote notebook details.
    • Automatic splitting of multi-notebook Deepnote project files into separate notebook files on open (shows created files).
  • Improvements

    • Explorer now groups files by project for clearer navigation.
    • Snapshots are notebook-scoped with safer restore behavior.
    • Improved save/self-write detection to reduce duplicate or leaked updates.

tkislan added 30 commits March 18, 2026 19:08
…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.
…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.
tkislan added 11 commits April 2, 2026 14:47
- 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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

This 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 deepnote.copyNotebookDetails command, enhances file-change handling (normalized self-write keys, applyNotebookEdits, save-based self-write detection), expands VS Code mocks (TabInputNotebook), and updates DI registrations and many unit tests to match the new behaviors.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • andyjakubowski
🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Updates Docs ⚠️ Warning PR adds substantial feature across 31 files but modifies zero documentation files. Update specs/architecture.md and README.md to document the single-notebook migration and new Copy Notebook Details command.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the primary change: migrating Deepnote notebook file handling to support single-notebook files instead of multi-notebook projects.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0%. Comparing base (eb8d457) to head (b73d313).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@     Coverage Diff     @@
##   main   #381   +/-   ##
===========================
===========================
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Filter 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 | 🟡 Minor

Rename 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

notebookId is extracted, then discarded.

handleSnapshotFileChange() derives a notebookId, and this method filters editors with it, but the queued operation only keeps projectId. 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 | 🟠 Major

Notebook-scoped reads dropped backward compatibility.

When notebookId is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3b7356a and 477b790.

📒 Files selected for processing (28)
  • build/mocha-esm-loader.js
  • package.json
  • package.nls.json
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/kernels/deepnote/deepnoteTestHelpers.node.ts
  • src/notebooks/deepnote/deepnoteActivationService.ts
  • src/notebooks/deepnote/deepnoteAutoSplitter.ts
  • src/notebooks/deepnote/deepnoteExplorerView.ts
  • src/notebooks/deepnote/deepnoteExplorerView.unit.test.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts
  • src/notebooks/deepnote/deepnoteNotebookInfoStatusBar.ts
  • src/notebooks/deepnote/deepnoteNotebookManager.ts
  • src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts
  • src/notebooks/deepnote/deepnoteSerializer.ts
  • src/notebooks/deepnote/deepnoteSerializer.unit.test.ts
  • src/notebooks/deepnote/deepnoteTreeDataProvider.ts
  • src/notebooks/deepnote/deepnoteTreeDataProvider.unit.test.ts
  • src/notebooks/deepnote/deepnoteTreeItem.ts
  • src/notebooks/deepnote/deepnoteTreeItem.unit.test.ts
  • src/notebooks/deepnote/snapshots/snapshotFiles.ts
  • src/notebooks/deepnote/snapshots/snapshotService.ts
  • src/notebooks/serviceRegistry.node.ts
  • src/notebooks/serviceRegistry.web.ts
  • src/notebooks/types.ts
  • src/platform/common/constants.ts
  • src/test/mocks/vsc/extHostedTypes.ts
  • src/test/vscode-mock.ts

Comment thread src/kernels/deepnote/deepnoteServerStarter.node.ts Outdated
Comment on lines +15 to +66
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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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
done

Repository: 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.

Suggested change
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.

Comment on lines +59 to +63
this.extensionContext.subscriptions.push(
workspace.onDidOpenNotebookDocument((doc) => {
void this.checkAndSplitIfNeeded(doc);
})
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +46 to +49
for (const notebook of extraNotebooks) {
const notebookSlug = this.slugifyNotebookName(notebook.name);
const newFileName = `${originalStem}_${notebookSlug}.deepnote`;
const newFileUri = Uri.joinPath(parentDir, newFileName);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +120 to +290
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';
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment thread src/notebooks/deepnote/deepnoteNotebookManager.ts Outdated
Comment thread src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts Outdated
Comment on lines 148 to +150
private async performInitialScan(): Promise<void> {
try {
await this.getDeepnoteProjectFiles();
await this.loadAllProjectFiles();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment thread src/notebooks/deepnote/deepnoteTreeItem.ts Outdated
Comment on lines +2547 to +2554
export class TabInputNotebook {
readonly uri: vscUri.URI;
readonly notebookType: string;

constructor(uri: vscUri.URI, notebookType: string) {
this.uri = uri;
this.notebookType = notebookType;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

URI key mismatch: snapshotSelfWriteUris populated without normalization.

onFileWritten callback stores uri.toString() (line 123), but consumeSelfWrite (line 173) checks using normalizeFileUri(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

📥 Commits

Reviewing files that changed from the base of the PR and between 477b790 and 1140fa0.

📒 Files selected for processing (2)
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts

Comment on lines +398 to +406
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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +131 to +143
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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (3)
src/notebooks/deepnote/deepnoteFileChangeWatcher.ts (1)

398-406: ⚠️ Potential issue | 🟡 Minor

Return after workspace.save() throws.

The false/undefined path 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 | 🟠 Major

Creating a notebook still falls back to the file’s default notebook.

This path appends a second notebook into the same .deepnote file and then reopens only fileUri. 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 | 🟠 Major

Duplicate reopens the original/default notebook, not the copy.

newNotebook is appended, but the reopen step still targets only fileUri. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1140fa0 and 966bf96.

📒 Files selected for processing (13)
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/notebooks/deepnote/deepnoteExplorerView.ts
  • src/notebooks/deepnote/deepnoteExplorerView.unit.test.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts
  • src/notebooks/deepnote/deepnoteKernelAutoSelector.node.ts
  • src/notebooks/deepnote/deepnoteNotebookManager.ts
  • src/notebooks/deepnote/deepnoteNotebookManager.unit.test.ts
  • src/notebooks/deepnote/deepnoteSerializer.ts
  • src/notebooks/deepnote/deepnoteSerializer.unit.test.ts
  • src/notebooks/deepnote/deepnoteTreeItem.ts
  • src/notebooks/deepnote/snapshots/snapshotService.ts
  • src/notebooks/types.ts

Comment on lines +446 to 462
/**
* 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> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +35 to +43
if (notebookId !== undefined) {
const exactMatch = notebookMap.get(notebookId);

if (exactMatch) {
return exactMatch;
}
}

return notebookMap.values().next().value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment on lines 35 to 40
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +53 to +58
} 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');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +415 to +418
// 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`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +773 to 779
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

This test never isolates noSnapshotWatcher.

noSnapshotWatcher is created here, but the fired event (snapshotOnDidChange) and the asserted counters still belong to the already-active snapshotWatcher. That means this can stay green without proving the new watcher ignores snapshot events. Give noSnapshotWatcher its own FileSystemWatcher mock/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

📥 Commits

Reviewing files that changed from the base of the PR and between 966bf96 and 367db44.

📒 Files selected for processing (1)
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts

Comment on lines +295 to +296
test('saving a non-deepnote notebook should not suppress FS change events', async function () {
this.timeout(8000);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +1618 to +1630
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);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/notebooks/deepnote/deepnoteExplorerView.ts (2)

288-289: ⚠️ Potential issue | 🟠 Major

Don't reopen the base file after duplication.

This writes the copy into the source .deepnote and then reopens fileUri again, 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 way createAndAddNotebookToProject() 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 | 🟠 Major

Pass the notebook target through this open path.

context.notebookId is 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 | 🔴 Critical

Make the split loop collision-safe and failure-safe.

Slug-only filenames still collide for duplicate names or the 'notebook' fallback, and one failed writeFile aborts 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 principle and Use 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 | 🟠 Major

Skip snapshots that already use notebook-scoped names.

snapshotGlob matches both legacy and new snapshot filenames, but extractVariantFromSnapshotFilename() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 367db44 and b73d313.

📒 Files selected for processing (5)
  • src/notebooks/deepnote/deepnoteAutoSplitter.ts
  • src/notebooks/deepnote/deepnoteExplorerView.ts
  • src/notebooks/deepnote/deepnoteExplorerView.unit.test.ts
  • src/notebooks/deepnote/deepnoteNotebookFileFactory.ts
  • src/notebooks/deepnote/deepnoteNotebookFileFactory.unit.test.ts

Comment on lines +70 to +75
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));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +547 to 549
public refreshTree(): void {
this.treeDataProvider.refresh();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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()).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant