Skip to content

fix: Fix deepnote notebook deserializer and file change watcher#363

Open
tkislan wants to merge 20 commits intomainfrom
tk/fix-project-notebook-picker
Open

fix: Fix deepnote notebook deserializer and file change watcher#363
tkislan wants to merge 20 commits intomainfrom
tk/fix-project-notebook-picker

Conversation

@tkislan
Copy link
Copy Markdown
Contributor

@tkislan tkislan commented Mar 18, 2026

Summary by CodeRabbit

  • Bug Fixes

    • More robust multi-notebook sync to prevent cross-notebook corruption and reduce erroneous saves
    • Improved self-write detection and safer disk-write/save sequencing
    • Prevent duplicate process output listeners for server monitoring
  • New Features

    • Pending notebook-resolution queue so explicit notebook intents are honored and consumed once
    • Serialization/deserialization now respects explicit notebook selection and better resolves target notebooks
  • Tests

    • Expanded unit tests for multi-notebook sync, queueing/consumption, selection clearing, and serialization flows

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

This PR makes notebook resolution explicit and adds a per-project pending notebook-resolution queue (2s TTL) with queue/consume semantics and clear-selection behavior. deserializeNotebook now accepts an optional notebookId and resolves selection from: a consumed pending hint, the manager’s current notebook ID constrained to open notebooks (or when none open), or a single open notebook fallback. Serialization uses manager.getCurrentNotebookId and may queue a resolution when no open notebooks exist. File-change sync was refactored: add applyNotebookEdits helper, normalize file URIs for self-write tracking, write canonical bytes to disk before save, and pass explicit notebook IDs into deserialization. Explorer open flow and tests updated accordingly.

Sequence Diagram

sequenceDiagram
    participant Watcher as FileChangeWatcher
    participant Manager as NotebookManager
    participant Serializer as NotebookSerializer
    participant Workspace as WorkspaceFS

    Note over Watcher,Manager: file change / manual sync triggered
    Watcher->>Manager: consumePendingNotebookResolution(projectId)
    alt pending resolution present
        Manager-->>Watcher: notebookId
    else no pending hint
        Manager-->>Watcher: undefined
    end
    Watcher->>Serializer: deserializeNotebook(bytes, token, notebookId?)
    alt notebookId not provided
        Serializer->>Manager: getCurrentNotebookId(projectId)
        Manager-->>Serializer: currentNotebookId | undefined
    end
    Serializer-->>Watcher: NotebookDocument edits/metadata
    Watcher->>Workspace: applyNotebookEdits(uri, edits)
    Workspace-->>Watcher: edits applied
    Watcher->>Serializer: serializeNotebook(document)
    Serializer->>Manager: getCurrentNotebookId(projectId)
    Serializer-->>Watcher: bytes
    Watcher->>Workspace: writeFile(fileUri, bytes)
    alt write success
        Watcher->>Watcher: markSelfWrite(normalizeFileUri(fileUri))
        Watcher->>Workspace: save(notebookUri)
    else write failure
        Watcher->>Watcher: consumeSelfWrite(normalizeFileUri(fileUri))
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 three new public API methods to IDeepnoteNotebookManager without updating CONTRIBUTING.md documentation. Update CONTRIBUTING.md Notebook Manager section to document the three new methods, pending resolution queue with TTL, and revised resolution precedence logic.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main focus: fixes to the deepnote notebook deserializer and file change watcher, matching the substantial changes across these components.

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

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 18, 2026
@tkislan tkislan marked this pull request as ready for review March 19, 2026 07:51
@tkislan tkislan requested a review from a team as a code owner March 19, 2026 07:51
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 19, 2026

Codecov Report

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

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

@tkislan tkislan changed the title fix: Fix deepnote notebook deserializer fix: Fix deepnote notebook deserializer and file change watcher Mar 19, 2026
@tkislan tkislan marked this pull request as draft March 19, 2026 14:01
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: 1

🤖 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 356-373: The writeFile catch path allows execution to continue to
the subsequent markSelfWrite and workspace.save calls which assume the file was
written; modify the write failure handler in deepnoteFileChangeWatcher (around
workspace.fs.writeFile) to return early after calling
this.consumeSelfWrite(fileUri) and logging the warning so we do not proceed to
markSelfWrite or call workspace.save on a failed write; ensure markSelfWrite and
consumeSelfWrite usage remains correct and only the successful-write path
reaches the save logic.
🪄 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: aacbc659-5d97-4eeb-9ad4-a231351e6a81

📥 Commits

Reviewing files that changed from the base of the PR and between 2de7885 and 9b5c107.

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

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)

336-373: ⚠️ Potential issue | 🔴 Critical

Guard the canonical rewrite against a second external save.

A later disk write between the initial readFile() and Line 358 gets overwritten here by the older snapshot we already deserialized. The queued follow-up sync will reread our overwrite, not the newer external bytes. Re-check the file immediately before writeFile(), and skip the canonical rewrite/save path if it changed again.

🤖 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 336 - 373,
Between the initial read and the canonical rewrite you must guard against a
second external save: before calling workspace.fs.writeFile(fileUri,
serializedBytes) (inside the try where serializeTokenSource and serializedBytes
are prepared), re-check the file's current mtime or contents (e.g., await
workspace.fs.stat(fileUri) or readFile) and compare to the snapshot used to
produce serializedBytes (newData / original mtime); if the on-disk file has
changed since we deserialized, call this.consumeSelfWrite(fileUri) as needed and
skip the canonical write/save path (return early) so we don't overwrite a newer
external change—make this check before markSelfWrite(fileUri) and before
workspace.save(notebook.uri), keeping serializeTokenSource, serializeNotebook,
markSelfWrite, consumeSelfWrite, workspace.fs.writeFile and workspace.save calls
intact.
🤖 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 557-564: The private helper normalizeFileUri(uri: Uri): string is
correctly implemented but is misplaced; move the normalizeFileUri method so it
follows the file's private-member alphabetical ordering (i.e., relocate it among
other private methods/fields to the position determined by its name) so the
class groups private members by accessibility then alphabetically, keeping its
implementation intact and updating surrounding spacing/comments as needed.
- Around line 371-377: When calling workspace.save(notebook.uri) in the
deepnoteFileChangeWatcher, handle the case where it resolves to undefined
(indicating save failed or no editor) instead of only catching exceptions: after
await workspace.save(notebook.uri) check the return value and if it is undefined
call this.consumeSelfWrite(fileUri) and log a warning (same as in the catch) so
the armed self-write marker set by this.markSelfWrite(fileUri) is cleared
immediately; ensure both the error catch block and the "undefined" branch clear
the marker to avoid dropping real FS events.

---

Outside diff comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 336-373: Between the initial read and the canonical rewrite you
must guard against a second external save: before calling
workspace.fs.writeFile(fileUri, serializedBytes) (inside the try where
serializeTokenSource and serializedBytes are prepared), re-check the file's
current mtime or contents (e.g., await workspace.fs.stat(fileUri) or readFile)
and compare to the snapshot used to produce serializedBytes (newData / original
mtime); if the on-disk file has changed since we deserialized, call
this.consumeSelfWrite(fileUri) as needed and skip the canonical write/save path
(return early) so we don't overwrite a newer external change—make this check
before markSelfWrite(fileUri) and before workspace.save(notebook.uri), keeping
serializeTokenSource, serializeNotebook, markSelfWrite, consumeSelfWrite,
workspace.fs.writeFile and workspace.save calls intact.
🪄 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: f0bec955-fde1-408e-9887-8738d66b52bc

📥 Commits

Reviewing files that changed from the base of the PR and between 9b5c107 and 77c0347.

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

tkislan added 2 commits March 20, 2026 08:10
…out block IDs and implement a valid project structure for testing
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: 1

🤖 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 1130-1139: The test currently asserts nfApplyEditCount === 0
immediately after detecting readSnapshot ran, which can miss an applyEdit
scheduled a tick later; after calling noFallbackOnDidChange.fire(...) and
awaiting waitFor(() => nfReadSnapshotCount >= 1), add a short post-read grace
wait (e.g. a small setTimeout delay or a brief waitFor that ensures no further
nfApplyEditCount increments occur) before asserting nfApplyEditCount is 0 so any
asynchronously scheduled applyEdit has time to run and be counted; target
symbols: noFallbackOnDidChange.fire, nfReadSnapshotCount, nfApplyEditCount,
readSnapshot, applyEdit, and waitFor.
🪄 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: 660447d0-1752-4128-8450-fefd098bb6a5

📥 Commits

Reviewing files that changed from the base of the PR and between 77c0347 and 6dc77a0.

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

tkislan added 2 commits March 23, 2026 09:54
…ration result and log warnings for undefined saves
…ngeWatcher to ensure proper handling of snapshot reads
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: 1

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)

1080-1143: 🧹 Nitpick | 🔵 Trivial

Dispose the ad-hoc watcher setup in finally.

If waitFor() or the assertion fails, this extra watcher and its emitters leak into later tests.

Proposed fix
-            noFallbackOnDidChange.fire(snapshotUri);
-
-            await waitFor(() => nfReadSnapshotCount >= 1);
-            await new Promise((resolve) => setTimeout(resolve, postSnapshotReadGraceMs));
-
-            assert.isAtLeast(nfReadSnapshotCount, 1, 'readSnapshot should be called');
-            assert.strictEqual(nfApplyEditCount, 0, 'applyEdit should NOT be called when no block IDs can be resolved');
-
-            for (const d of noFallbackDisposables) {
-                d.dispose();
-            }
-            noFallbackOnDidChange.dispose();
-            noFallbackOnDidCreate.dispose();
+            try {
+                noFallbackOnDidChange.fire(snapshotUri);
+
+                await waitFor(() => nfReadSnapshotCount >= 1);
+                await new Promise((resolve) => setTimeout(resolve, postSnapshotReadGraceMs));
+
+                assert.isAtLeast(nfReadSnapshotCount, 1, 'readSnapshot should be called');
+                assert.strictEqual(nfApplyEditCount, 0, 'applyEdit should NOT be called when no block IDs can be resolved');
+            } finally {
+                for (const d of noFallbackDisposables) {
+                    d.dispose();
+                }
+                noFallbackOnDidChange.dispose();
+                noFallbackOnDidCreate.dispose();
+            }
🤖 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
1080 - 1143, The test creates ephemeral watchers and emitters
(noFallbackOnDidChange, noFallbackOnDidCreate, noFallbackDisposables, nfWatcher)
but only disposes them at the end of the happy path, risking leaks if
waitFor/assertions throw; wrap the activation, firing and assertions in a
try/finally and in finally call nfWatcher.dispose() or nfWatcher.deactivate()
(whichever exists), dispose all entries in noFallbackDisposables, and call
noFallbackOnDidChange.dispose() and noFallbackOnDidCreate.dispose() to ensure
the ad-hoc watcher setup is always cleaned up.
🤖 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 355-381: The second call to markSelfWrite should be removed so we
only arm selfWriteCounts for the explicit workspace.fs.writeFile operation;
locate the block where markSelfWrite(fileUri) is called right before awaiting
workspace.save(notebook.uri) (paired with consumeSelfWrite(fileUri) on failures)
and delete that second markSelfWrite invocation, keeping the consumeSelfWrite
calls on the save error/unsaved paths and leaving the first markSelfWrite before
workspace.fs.writeFile and the contentActuallyChanged handling unchanged.

---

Outside diff comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 1080-1143: The test creates ephemeral watchers and emitters
(noFallbackOnDidChange, noFallbackOnDidCreate, noFallbackDisposables, nfWatcher)
but only disposes them at the end of the happy path, risking leaks if
waitFor/assertions throw; wrap the activation, firing and assertions in a
try/finally and in finally call nfWatcher.dispose() or nfWatcher.deactivate()
(whichever exists), dispose all entries in noFallbackDisposables, and call
noFallbackOnDidChange.dispose() and noFallbackOnDidCreate.dispose() to ensure
the ad-hoc watcher setup is always cleaned up.
🪄 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: f1db496a-5f85-4322-b73b-faa657bc8a9d

📥 Commits

Reviewing files that changed from the base of the PR and between 8898be9 and 8d061ac.

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

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.

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

559-566: 🧹 Nitpick | 🔵 Trivial

normalizeFileUri is misplaced — should follow alphabetical ordering.

Implementation is correct. Position is not. Should come after markSelfWrite and before outputsMatch.

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/notebooks/deepnote/deepnoteFileChangeWatcher.ts` around lines 559 - 566,
Move the private method normalizeFileUri so it follows the existing private
method markSelfWrite and comes immediately before outputsMatch to satisfy the
alphabetical ordering rule; keep the method signature and accessibility (private
normalizeFileUri(uri: Uri): string) unchanged and only relocate its declaration
within the class so ordering is markSelfWrite -> normalizeFileUri ->
outputsMatch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 559-566: Move the private method normalizeFileUri so it follows
the existing private method markSelfWrite and comes immediately before
outputsMatch to satisfy the alphabetical ordering rule; keep the method
signature and accessibility (private normalizeFileUri(uri: Uri): string)
unchanged and only relocate its declaration within the class so ordering is
markSelfWrite -> normalizeFileUri -> outputsMatch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 341457a5-34c2-4736-9dd1-1f1af080e78e

📥 Commits

Reviewing files that changed from the base of the PR and between 8d061ac and 5388ead.

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

@tkislan tkislan marked this pull request as ready for review March 26, 2026 13:06
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 26, 2026
tkislan added 4 commits March 26, 2026 14:06
… 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.
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: 1

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/deepnoteSerializer.ts (1)

97-110: ⚠️ Potential issue | 🟠 Major

Recover from stale remembered notebook IDs.

Now that manager state wins first, a deleted or renamed notebook leaves resolvedNotebookId truthy but unmatched, and this path throws instead of falling back. That can make a fresh open fail until the cached selection is cleared.

💡 Minimal fix
-            const selectedNotebook = resolvedNotebookId
-                ? deepnoteFile.project.notebooks.find((nb) => nb.id === resolvedNotebookId)
-                : this.findDefaultNotebook(deepnoteFile);
+            let selectedNotebook = resolvedNotebookId
+                ? deepnoteFile.project.notebooks.find((nb) => nb.id === resolvedNotebookId)
+                : undefined;
+
+            if (!selectedNotebook && notebookId === undefined) {
+                if (
+                    resolvedNotebookId &&
+                    this.notebookManager.getTheSelectedNotebookForAProject(projectId) === resolvedNotebookId
+                ) {
+                    logger.warn(
+                        `DeepnoteSerializer: Notebook ${resolvedNotebookId} no longer exists in project ${projectId}; falling back`
+                    );
+                    this.notebookManager.clearNotebookSelection(projectId);
+                }
+                selectedNotebook = this.findDefaultNotebook(deepnoteFile);
+            }
 
             if (!selectedNotebook) {
                 throw new Error(l10n.t('No notebook selected or found'));
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notebooks/deepnote/deepnoteSerializer.ts` around lines 97 - 110, The code
currently throws when a truthy resolvedNotebookId doesn't match any notebook
(stale selection); update the selection logic in DeepnoteSerializer so that
after computing resolvedNotebookId (from findCurrentNotebookId), if
resolvedNotebookId is set but deepnoteFile.project.notebooks.find(...) returns
undefined, fall back to this.findDefaultNotebook(deepnoteFile) instead of
throwing; ensure selectedNotebook is assigned that fallback and only throw if no
notebook is found after the fallback, referencing resolvedNotebookId,
selectedNotebook, findCurrentNotebookId, deepnoteFile, and findDefaultNotebook.
🤖 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 238-247: The unconditional call to
this.notebookManager.clearNotebookSelection(projectId) in
deepnoteFileChangeWatcher.ts removes the user's remembered per-project notebook
choice; instead either remove this clear or replace it with a non-destructive
approach: add a scoped/signal flag to NotebookManager (e.g.,
suppressSelectionLookupForSync or pass a syncContext parameter into
selectNotebookForProject / lookup methods) so the file-change sync can ignore
the stored selection without deleting it, and update the affected code path that
iterates affectedNotebooks to use that flag/parameter rather than calling
clearNotebookSelection(projectId).

---

Outside diff comments:
In `@src/notebooks/deepnote/deepnoteSerializer.ts`:
- Around line 97-110: The code currently throws when a truthy resolvedNotebookId
doesn't match any notebook (stale selection); update the selection logic in
DeepnoteSerializer so that after computing resolvedNotebookId (from
findCurrentNotebookId), if resolvedNotebookId is set but
deepnoteFile.project.notebooks.find(...) returns undefined, fall back to
this.findDefaultNotebook(deepnoteFile) instead of throwing; ensure
selectedNotebook is assigned that fallback and only throw if no notebook is
found after the fallback, referencing resolvedNotebookId, selectedNotebook,
findCurrentNotebookId, deepnoteFile, and findDefaultNotebook.
🪄 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: 495a5709-4444-44a8-8593-2f7e3d652f86

📥 Commits

Reviewing files that changed from the base of the PR and between 5388ead and c160ae3.

📒 Files selected for processing (7)
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.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/types.ts

…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.
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: 4

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)

1141-1204: 🧹 Nitpick | 🔵 Trivial

Guard the extra watcher cleanup with try/finally.

This test only disposes its local watcher and emitters after the assertions. If it fails early, those listeners bleed into later cases and turn one failure into a cascade.

🤖 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
1141 - 1204, The test leaks its local listeners when assertions fail because the
disposal of noFallbackDisposables, noFallbackOnDidChange, and
noFallbackOnDidCreate happens after assertions; wrap the test body (from firing
noFallbackOnDidChange through the asserts) in a try/finally and move the cleanup
into the finally block so you always call d.dispose() for each item in
noFallbackDisposables and call noFallbackOnDidChange.dispose() and
noFallbackOnDidCreate.dispose() even if an assertion throws; keep the existing
waitFor and timing logic inside the try so the finally always runs.
🤖 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-410: The loop that disposes entries from disposablesByFile for
a given fileKey should guard each call so one thrown error doesn't abort
remaining disposals; in the block where you iterate existing (for (const d of
existing) { d.dispose(); }), wrap each d.dispose() in a try/catch and handle/log
the error (e.g. using this.logger.error or console.error) while continuing the
loop so all disposables are attempted to be cleaned up.

In `@src/kernels/deepnote/deepnoteTestHelpers.node.ts`:
- Line 56: The hardcoded magic number 10 used in the getMaxListeners mock should
be extracted to a named constant; define something like DEFAULT_MAX_LISTENERS =
10 near the top of this module and replace the literal in getMaxListeners with
the constant to follow the guideline for extracting magic numbers (update the
exported/used mocks that reference getMaxListeners if any).

In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 1353-1398: The test currently only asserts that
mockedNotebookManager.clearNotebookSelection('project-1') was called at some
point, which doesn't ensure it happened before the first sync; modify the test
to record call order and assert clearNotebookSelection occurs before the first
applyEdit invocation: capture call timestamps/order (e.g. by spying on
mockedNotebookManager.clearNotebookSelection and the function that increments
applyEditCount or on applyEdit itself), fire onDidChangeFile, await waitFor(()
=> applyEditCount >= 2), and then assert the clearNotebookSelection call index
is less than the first applyEdit call index (or use a call order verifier) so
the clear happens before the initial applyEdit/sync.

In `@src/test/mocks/vsc/extHostedTypes.ts`:
- Around line 798-818: The mock leaves entries in _textEdits/_notebookEdits when
clearing edits, causing has()/size to report a dirty URI; instead of setting
data.edits = undefined or [] for the cases where edits is undefined or
edits.length === 0, remove the entire entry from _textEdits
(this._textEdits.delete(uriKey)) and also remove any notebook entry
(this._notebookEdits.delete(uriKey)) so the maps reflect a cleared URI; update
the logic around the existing uriKey/data/_seqPool handling to delete rather
than retain empty or undefined edit records.

---

Outside diff comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 1141-1204: The test leaks its local listeners when assertions fail
because the disposal of noFallbackDisposables, noFallbackOnDidChange, and
noFallbackOnDidCreate happens after assertions; wrap the test body (from firing
noFallbackOnDidChange through the asserts) in a try/finally and move the cleanup
into the finally block so you always call d.dispose() for each item in
noFallbackDisposables and call noFallbackOnDidChange.dispose() and
noFallbackOnDidCreate.dispose() even if an assertion throws; keep the existing
waitFor and timing logic inside the try so the finally always runs.
🪄 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: aad6e30f-cffd-486f-b4b5-75193eeb82a0

📥 Commits

Reviewing files that changed from the base of the PR and between c160ae3 and 0b2504a.

📒 Files selected for processing (4)
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/kernels/deepnote/deepnoteTestHelpers.node.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts
  • src/test/mocks/vsc/extHostedTypes.ts

Comment on lines +1353 to +1398
test('should clear notebook selection before processing file change', async () => {
const basePath = Uri.file('/workspace/multi.deepnote');
const uriNb1 = basePath.with({ query: 'a=1' });
const uriNb2 = basePath.with({ query: 'b=2' });

const notebook1 = createMockNotebook({
uri: uriNb1,
metadata: {
deepnoteProjectId: 'project-1',
deepnoteNotebookId: 'notebook-1'
},
cells: [
{
metadata: { id: 'block-nb1' },
outputs: [],
kind: NotebookCellKind.Code,
document: { getText: () => 'print("nb1-old")' }
}
]
});

const notebook2 = createMockNotebook({
uri: uriNb2,
metadata: {
deepnoteProjectId: 'project-1',
deepnoteNotebookId: 'notebook-2'
},
cells: [
{
metadata: { id: 'block-nb2' },
outputs: [],
kind: NotebookCellKind.Code,
document: { getText: () => 'print("nb2-old")' }
}
]
});

when(mockedVSCodeNamespaces.workspace.notebookDocuments).thenReturn([notebook1, notebook2]);
setupMockFs(multiNotebookYaml);

onDidChangeFile.fire(basePath);

await waitFor(() => applyEditCount >= 2);

verify(mockedNotebookManager.clearNotebookSelection('project-1')).once();
});
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

This does not verify “before.”

verify(...).once() only proves clearNotebookSelection('project-1') happened sometime. If it runs after the first applyEdit(), this test still passes and the stale-selection regression slips through. Record call order and assert the clear happens before the first sync.

🤖 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
1353 - 1398, The test currently only asserts that
mockedNotebookManager.clearNotebookSelection('project-1') was called at some
point, which doesn't ensure it happened before the first sync; modify the test
to record call order and assert clearNotebookSelection occurs before the first
applyEdit invocation: capture call timestamps/order (e.g. by spying on
mockedNotebookManager.clearNotebookSelection and the function that increments
applyEditCount or on applyEdit itself), fire onDidChangeFile, await waitFor(()
=> applyEditCount >= 2), and then assert the clearNotebookSelection call index
is less than the first applyEdit call index (or use a call order verifier) so
the clear happens before the initial applyEdit/sync.

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

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

245-254: ⚠️ Potential issue | 🟠 Major

Don't turn sync into a persistent selection reset.

clearNotebookSelection() deletes the remembered per-project notebook choice, and this flow never restores it. The sync itself already bypasses shared selection state at Line 305 by passing deepnoteNotebookId directly into deserializeNotebook(...), so this mutation only discards the user's choice. If VS Code-driven reloads still need different lookup rules, make that a scoped override on lookup instead of deleting the saved selection.

🤖 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 245 - 254,
The current loop calls this.notebookManager.clearNotebookSelection(projectId)
for the first affected notebook which permanently deletes the per-project
selection; remove that mutation so we don't discard the user's saved choice.
Instead, stop calling clearNotebookSelection in deepnoteFileChangeWatcher; when
a VS Code-triggered re-deserialization needs to bypass shared selection, use the
existing mechanism of passing deepnoteNotebookId into deserializeNotebook(...)
(and, if needed, add a scoped lookup override in the notebookManager lookup
path) rather than clearing state. Ensure references: affectedNotebooks loop,
clearNotebookSelection, deserializeNotebook(deepnoteNotebookId), and
notebookManager are used to locate and adjust the change.
src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts (1)

1352-1397: ⚠️ Potential issue | 🟡 Minor

This still doesn't prove “before.”

verify(clearNotebookSelection('project-1')).once() only shows the call happened sometime. The test still passes if the clear runs after the first sync edit. Record call order against the first applyNotebookEdits() invocation and assert the clear happens earlier.

🤖 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
1352 - 1397, The test currently only verifies
clearNotebookSelection('project-1') was called, not that it happened before
applyNotebookEdits; to fix this, record call ordering by stubbing
mockedNotebookManager.clearNotebookSelection and
mockedNotebookManager.applyNotebookEdits to push markers into a local callOrder
array when invoked (e.g.,
when(mockedNotebookManager.clearNotebookSelection).thenCall(() => {
callOrder.push('clear'); ... }) and similarly for applyNotebookEdits pushing
'apply' while still preserving their original behavior and applyEditCount
updates), then after the watcher runs assert callOrder.indexOf('clear') <
callOrder.indexOf('apply') in addition to the existing verify on
clearNotebookSelection so the test proves the clear happened before the first
applyNotebookEdits call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 245-254: The current loop calls
this.notebookManager.clearNotebookSelection(projectId) for the first affected
notebook which permanently deletes the per-project selection; remove that
mutation so we don't discard the user's saved choice. Instead, stop calling
clearNotebookSelection in deepnoteFileChangeWatcher; when a VS Code-triggered
re-deserialization needs to bypass shared selection, use the existing mechanism
of passing deepnoteNotebookId into deserializeNotebook(...) (and, if needed, add
a scoped lookup override in the notebookManager lookup path) rather than
clearing state. Ensure references: affectedNotebooks loop,
clearNotebookSelection, deserializeNotebook(deepnoteNotebookId), and
notebookManager are used to locate and adjust the change.

In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.ts`:
- Around line 1352-1397: The test currently only verifies
clearNotebookSelection('project-1') was called, not that it happened before
applyNotebookEdits; to fix this, record call ordering by stubbing
mockedNotebookManager.clearNotebookSelection and
mockedNotebookManager.applyNotebookEdits to push markers into a local callOrder
array when invoked (e.g.,
when(mockedNotebookManager.clearNotebookSelection).thenCall(() => {
callOrder.push('clear'); ... }) and similarly for applyNotebookEdits pushing
'apply' while still preserving their original behavior and applyEditCount
updates), then after the watcher runs assert callOrder.indexOf('clear') <
callOrder.indexOf('apply') in addition to the existing verify on
clearNotebookSelection so the test proves the clear happened before the first
applyNotebookEdits call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5f8aa2fa-2114-45bb-8691-306a85b05b5a

📥 Commits

Reviewing files that changed from the base of the PR and between 0b2504a and 9d5c6c0.

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

@tkislan tkislan requested a review from dinohamzic March 29, 2026 20:07
dinohamzic
dinohamzic previously approved these changes Mar 30, 2026
Copy link
Copy Markdown
Contributor

@dinohamzic dinohamzic left a comment

Choose a reason for hiding this comment

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

Nice, this seems to be doing the trick. 🚀

Code looks good overall, but I don't have enough VS Code extension specific knowledge to get into the details.

…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.
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/notebooks/deepnote/deepnoteNotebookManager.ts (1)

167-177: 🧹 Nitpick | 🔵 Trivial

Method ordering: hasInitNotebookBeenRun and markInitNotebookAsRun should precede queueNotebookResolution.

Alphabetically: has... < mark... < queue....

🤖 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 167 - 177,
Reorder the methods so hasInitNotebookBeenRun and markInitNotebookAsRun appear
before queueNotebookResolution in the DeepnoteNotebookManager class: move the
hasInitNotebookBeenRun(projectId: string): boolean and
markInitNotebookAsRun(projectId: string): void method definitions to precede the
queueNotebookResolution method to match the requested alphabetical ordering and
logical grouping.
src/notebooks/deepnote/deepnoteSerializer.unit.test.ts (1)

309-457: 🛠️ Refactor suggestion | 🟠 Major

Extract a NotebookDocument fixture helper.

These resolver tests duplicate the same fake NotebookDocument shape four times. One helper would make API-shape changes much cheaper and reduce fixture drift.

Suggested refactor
-const notebookA = {
-    then: undefined,
-    notebookType: 'deepnote',
-    metadata: {
-        deepnoteProjectId: 'project-123',
-        deepnoteNotebookId: 'notebook-a'
-    },
-    uri: {} as any,
-    version: 1,
-    isDirty: false,
-    isUntitled: false,
-    isClosed: false,
-    cellCount: 0,
-    cellAt: () => ({}) as any,
-    getCells: () => [],
-    save: async () => true
-} as NotebookDocument;
-const notebookB = {
-    then: undefined,
-    notebookType: 'deepnote',
-    metadata: {
-        deepnoteProjectId: 'project-123',
-        deepnoteNotebookId: 'notebook-b'
-    },
-    uri: {} as any,
-    version: 1,
-    isDirty: false,
-    isUntitled: false,
-    isClosed: false,
-    cellCount: 0,
-    cellAt: () => ({}) as any,
-    getCells: () => [],
-    save: async () => true
-} as NotebookDocument;
+const notebookA = createNotebookDocument('project-123', 'notebook-a');
+const notebookB = createNotebookDocument('project-123', 'notebook-b');
function createNotebookDocument(projectId: string, notebookId: string): NotebookDocument {
    return {
        then: undefined,
        notebookType: 'deepnote',
        metadata: {
            deepnoteProjectId: projectId,
            deepnoteNotebookId: notebookId
        },
        uri: {} as any,
        version: 1,
        isDirty: false,
        isUntitled: false,
        isClosed: false,
        cellCount: 0,
        cellAt: () => ({}) as any,
        getCells: () => [],
        save: async () => true
    } as NotebookDocument;
}
As per coding guidelines, "Extract duplicate logic into helper methods to prevent drift following DRY principle".
🤖 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/deepnoteExplorerView.ts`:
- Around line 520-523: Move the private method registerNotebookOpenIntent so
that it appears alphabetically after refreshExplorer; specifically reorder the
methods in the class so refreshExplorer comes first and
registerNotebookOpenIntent (the method that calls
this.manager.queueNotebookResolution and this.manager.selectNotebookForProject)
immediately follows, preserving the method body and visibility.

In `@src/notebooks/deepnote/deepnoteSerializer.ts`:
- Around line 556-569: The method findOpenNotebookIds is out of the requested
alphabetical ordering and should be moved to follow findNotebookUri; relocate
the entire private method findOpenNotebookIds so it comes immediately after the
findNotebookUri method (keeping its implementation unchanged) to satisfy the
alphabetical ordering rule (findDefault... < findNotebook... < findOpen...).

In `@src/notebooks/deepnote/deepnoteSerializer.unit.test.ts`:
- Around line 254-258: The test currently asserts notebook contents by array
index which couples correctness to ordering; update the assertions to locate
notebooks by their unique id instead. After calling
serializer.serializeNotebook(...) and deserializeDeepnoteFile(...), find the
notebook object in serializedProject.project.notebooks whose id matches the
expected id from mockNotebookData (use the id fields on
mockNotebookData.notebooks) and then assert that that
notebook.blocks?.[0].content equals the expected string; reference
serializer.serializeNotebook, deserializeDeepnoteFile, mockNotebookData and
serializedProject.project.notebooks to locate the code to change.

---

Outside diff comments:
In `@src/notebooks/deepnote/deepnoteNotebookManager.ts`:
- Around line 167-177: Reorder the methods so hasInitNotebookBeenRun and
markInitNotebookAsRun appear before queueNotebookResolution in the
DeepnoteNotebookManager class: move the hasInitNotebookBeenRun(projectId:
string): boolean and markInitNotebookAsRun(projectId: string): void method
definitions to precede the queueNotebookResolution method to match the requested
alphabetical ordering and logical grouping.
🪄 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: 6b99eb80-8229-42c1-a45f-8a875f67b96e

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5c6c0 and 44bd482.

📒 Files selected for processing (9)
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/notebooks/deepnote/deepnoteExplorerView.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.ts
  • src/notebooks/deepnote/deepnoteFileChangeWatcher.unit.test.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/types.ts

Comment on lines +520 to +523
private registerNotebookOpenIntent(projectId: string, notebookId: string): void {
this.manager.queueNotebookResolution(projectId, notebookId);
this.manager.selectNotebookForProject(projectId, notebookId);
}
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

Method ordering: registerNotebookOpenIntent should follow refreshExplorer alphabetically.

Private methods should be ordered alphabetically. register... comes after refresh....

🤖 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 520 - 523, Move
the private method registerNotebookOpenIntent so that it appears alphabetically
after refreshExplorer; specifically reorder the methods in the class so
refreshExplorer comes first and registerNotebookOpenIntent (the method that
calls this.manager.queueNotebookResolution and
this.manager.selectNotebookForProject) immediately follows, preserving the
method body and visibility.

Comment on lines +254 to +258
const result = await serializer.serializeNotebook(mockNotebookData as any, {} as any);
const serializedProject = deserializeDeepnoteFile(new TextDecoder().decode(result));

assert.strictEqual(serializedProject.project.notebooks[0].blocks?.[0].content, 'print("hello")');
assert.strictEqual(serializedProject.project.notebooks[1].blocks?.[0].content, '# Updated second 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.

⚠️ Potential issue | 🟡 Minor

Assert by notebook ID, not array position.

This test is about notebook selection, but the current assertions also lock in project.notebooks ordering. A harmless reorder would fail the test for the wrong reason.

Suggested change
 const result = await serializer.serializeNotebook(mockNotebookData as any, {} as any);
 const serializedProject = deserializeDeepnoteFile(new TextDecoder().decode(result));

-assert.strictEqual(serializedProject.project.notebooks[0].blocks?.[0].content, 'print("hello")');
-assert.strictEqual(serializedProject.project.notebooks[1].blocks?.[0].content, '# Updated second notebook');
+const firstNotebook = serializedProject.project.notebooks.find((notebook) => notebook.id === 'notebook-1');
+const secondNotebook = serializedProject.project.notebooks.find((notebook) => notebook.id === 'notebook-2');
+
+assert.strictEqual(firstNotebook?.blocks?.[0].content, 'print("hello")');
+assert.strictEqual(secondNotebook?.blocks?.[0].content, '# Updated second notebook');
📝 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
const result = await serializer.serializeNotebook(mockNotebookData as any, {} as any);
const serializedProject = deserializeDeepnoteFile(new TextDecoder().decode(result));
assert.strictEqual(serializedProject.project.notebooks[0].blocks?.[0].content, 'print("hello")');
assert.strictEqual(serializedProject.project.notebooks[1].blocks?.[0].content, '# Updated second notebook');
const result = await serializer.serializeNotebook(mockNotebookData as any, {} as any);
const serializedProject = deserializeDeepnoteFile(new TextDecoder().decode(result));
const firstNotebook = serializedProject.project.notebooks.find((notebook) => notebook.id === 'notebook-1');
const secondNotebook = serializedProject.project.notebooks.find((notebook) => notebook.id === 'notebook-2');
assert.strictEqual(firstNotebook?.blocks?.[0].content, 'print("hello")');
assert.strictEqual(secondNotebook?.blocks?.[0].content, '# Updated second notebook');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notebooks/deepnote/deepnoteSerializer.unit.test.ts` around lines 254 -
258, The test currently asserts notebook contents by array index which couples
correctness to ordering; update the assertions to locate notebooks by their
unique id instead. After calling serializer.serializeNotebook(...) and
deserializeDeepnoteFile(...), find the notebook object in
serializedProject.project.notebooks whose id matches the expected id from
mockNotebookData (use the id fields on mockNotebookData.notebooks) and then
assert that that notebook.blocks?.[0].content equals the expected string;
reference serializer.serializeNotebook, deserializeDeepnoteFile,
mockNotebookData and serializedProject.project.notebooks to locate the code to
change.

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)

550-557: 🧹 Nitpick | 🔵 Trivial

Missing self-write consumption if workspace.save throws.

Line 555 consumes, but then immediately re-throws. The caller (drainQueue) catches and logs but doesn't propagate. This is fine, but the throw after consume is slightly unusual—could just log and return.

Consider returning instead of throwing
         } catch (error) {
             this.consumeSelfWrite(notebook.uri);
-            throw error;
+            logger.warn(`[FileChangeWatcher] Save after snapshot update failed: ${notebook.uri.path}`, error);
+            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 550 - 557,
In the save block inside deepnoteFileChangeWatcher, ensure self-write is always
consumed but don't re-throw from the catch — after calling
this.consumeSelfWrite(notebook.uri) on workspace.save failure, log the error and
return (or otherwise swallow it) instead of re-throwing so drainQueue doesn't
get unusual thrown errors; locate the try/catch around workspace.save and
replace the "throw error" behavior with logging and an early return while
keeping calls to this.markSelfWrite and this.consumeSelfWrite.
♻️ Duplicate comments (3)
src/notebooks/deepnote/deepnoteFileChangeWatcher.ts (2)

566-573: 🧹 Nitpick | 🔵 Trivial

normalizeFileUri placement breaks alphabetical order.

Should appear between markSelfWrite and outputsMatch. Already flagged in past review.

🤖 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 566 - 573,
The normalizeFileUri method is out of alphabetical order; move the private
method normalizeFileUri so it appears immediately after markSelfWrite and before
outputsMatch to restore the correct alphabetical ordering of methods (keep the
method signature and implementation unchanged, just relocate it between
markSelfWrite and outputsMatch).

381-392: ⚠️ Potential issue | 🟠 Major

Double markSelfWrite may cause marker leak if events coalesce.

Past review flagged this: writeFile (line 366) and workspace.save (line 381) both arm self-write. If FileSystemWatcher coalesces them into one event, the second marker persists 30s, suppressing the next real external change.

Consider keeping only the writeFile marker and letting workspace.save events pass through contentActuallyChanged().

Proposed fix
             // Save to clear dirty state. VS Code serializes (same bytes) and sees the
             // mtime from our recent write, so no "content is newer" conflict.
-            this.markSelfWrite(fileUri);
             try {
                 const saved = await workspace.save(notebook.uri);
                 if (!saved) {
-                    this.consumeSelfWrite(fileUri);
                     logger.warn(`[FileChangeWatcher] Save after sync write returned undefined: ${notebook.uri.path}`);
                     return;
                 }
             } catch (saveError) {
-                this.consumeSelfWrite(fileUri);
                 logger.warn(`[FileChangeWatcher] Save after sync write failed: ${notebook.uri.path}`, saveError);
             }
🤖 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 381 - 392,
The problem is duplicate self-write markers: markSelfWrite is called both in
writeFile and again around workspace.save which can leave an extra marker if
file watcher coalesces events; remove the second call to markSelfWrite inside
the workspace.save try/catch (and only call consumeSelfWrite on errors or when
you know you created a marker) so that writeFile is the single place that arms
the marker and workspace.save events are allowed to be processed by
contentActuallyChanged(); update uses of consumeSelfWrite in the save error path
to only clear markers actually created by writeFile to avoid leaking markers.
src/notebooks/deepnote/deepnoteSerializer.ts (1)

557-573: 🧹 Nitpick | 🔵 Trivial

Method ordering: findOpenNotebookIds should follow findNotebookUri alphabetically.

Per coding guidelines, methods ordered by accessibility then alphabetically. findOpen... > findNotebook....

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notebooks/deepnote/deepnoteSerializer.ts` around lines 557 - 573, Move
the private method findOpenNotebookIds so it appears alphabetically after
findNotebookUri within the class, keeping its private accessibility and current
implementation unchanged; locate the definitions for findNotebookUri and
findOpenNotebookIds in deepnoteSerializer.ts and reorder them so the methods are
grouped by accessibility and sorted alphabetically (findNotebookUri then
findOpenNotebookIds).
🤖 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/deepnoteSerializer.ts`:
- Around line 558-572: Return currently spreads a Set created by
workspace.notebookDocuments.reduce into an array; for clarity replace the final
spread with Array.from to convert the Set to an array. Locate the reducer that
iterates over workspace.notebookDocuments (checking doc.notebookType,
doc.metadata.deepnoteProjectId, and parsing doc.metadata.deepnoteNotebookId) and
change the return from [...<Set>] to Array.from(<Set>) so the intent is clearer
while keeping the same logic.
- Around line 196-214: The code currently calls
notebookManager.consumePendingNotebookResolution(projectId) which removes the
pending hint before validation; change this to first peek non-destructively
(e.g. notebookManager.peekPendingNotebookResolution(projectId) or
notebookManager.getPendingNotebookResolution(projectId)) to obtain
pendingNotebookId, then perform the existence/validation checks and only call
notebookManager.consumePendingNotebookResolution(projectId) to remove it after
the notebook is confirmed valid; update findCurrentNotebookId to use peek/get
first and consume only on successful validation so the hint isn't lost on
validation failure.

---

Outside diff comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 550-557: In the save block inside deepnoteFileChangeWatcher,
ensure self-write is always consumed but don't re-throw from the catch — after
calling this.consumeSelfWrite(notebook.uri) on workspace.save failure, log the
error and return (or otherwise swallow it) instead of re-throwing so drainQueue
doesn't get unusual thrown errors; locate the try/catch around workspace.save
and replace the "throw error" behavior with logging and an early return while
keeping calls to this.markSelfWrite and this.consumeSelfWrite.

---

Duplicate comments:
In `@src/notebooks/deepnote/deepnoteFileChangeWatcher.ts`:
- Around line 566-573: The normalizeFileUri method is out of alphabetical order;
move the private method normalizeFileUri so it appears immediately after
markSelfWrite and before outputsMatch to restore the correct alphabetical
ordering of methods (keep the method signature and implementation unchanged,
just relocate it between markSelfWrite and outputsMatch).
- Around line 381-392: The problem is duplicate self-write markers:
markSelfWrite is called both in writeFile and again around workspace.save which
can leave an extra marker if file watcher coalesces events; remove the second
call to markSelfWrite inside the workspace.save try/catch (and only call
consumeSelfWrite on errors or when you know you created a marker) so that
writeFile is the single place that arms the marker and workspace.save events are
allowed to be processed by contentActuallyChanged(); update uses of
consumeSelfWrite in the save error path to only clear markers actually created
by writeFile to avoid leaking markers.

In `@src/notebooks/deepnote/deepnoteSerializer.ts`:
- Around line 557-573: Move the private method findOpenNotebookIds so it appears
alphabetically after findNotebookUri within the class, keeping its private
accessibility and current implementation unchanged; locate the definitions for
findNotebookUri and findOpenNotebookIds in deepnoteSerializer.ts and reorder
them so the methods are grouped by accessibility and sorted alphabetically
(findNotebookUri then findOpenNotebookIds).
🪄 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: e4305fa8-0bd2-42d0-b46f-76b9bab46f7f

📥 Commits

Reviewing files that changed from the base of the PR and between 44bd482 and 71579cf.

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

Comment on lines 196 to 214
findCurrentNotebookId(projectId: string): string | undefined {
// Prefer the active notebook editor when it matches the project
const activeEditorNotebook = window.activeNotebookEditor?.notebook;

if (
activeEditorNotebook?.notebookType === 'deepnote' &&
activeEditorNotebook.metadata?.deepnoteProjectId === projectId &&
activeEditorNotebook.metadata?.deepnoteNotebookId
) {
return activeEditorNotebook.metadata.deepnoteNotebookId;
}
const pendingNotebookId = this.notebookManager.consumePendingNotebookResolution(projectId);
const openNotebookIds = this.findOpenNotebookIds(projectId);
const currentNotebookId = this.notebookManager.getCurrentNotebookId(projectId);

// Check the manager's stored selection - this should be set when opening from explorer
const storedNotebookId = this.notebookManager.getTheSelectedNotebookForAProject(projectId);
if (pendingNotebookId) {
return pendingNotebookId;
}

if (storedNotebookId) {
return storedNotebookId;
if (currentNotebookId && (openNotebookIds.length === 0 || openNotebookIds.includes(currentNotebookId))) {
return currentNotebookId;
}

// Fallback: Check if there's an active notebook document for this project
const openNotebook = workspace.notebookDocuments.find(
(doc) => doc.notebookType === 'deepnote' && doc.metadata?.deepnoteProjectId === projectId
);
if (openNotebookIds.length === 1) {
return openNotebookIds[0];
}

return openNotebook?.metadata?.deepnoteNotebookId;
return 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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if any callers handle the case where resolved notebook ID is not found in file
rg -n 'findCurrentNotebookId|consumePendingNotebookResolution' --type ts -C3

Repository: deepnote/vscode-deepnote

Length of output: 19082


🏁 Script executed:

# Check how the returned notebook ID is used after findCurrentNotebookId is called
sed -n '85,115p' src/notebooks/deepnote/deepnoteSerializer.ts

Repository: deepnote/vscode-deepnote

Length of output: 1291


🏁 Script executed:

# Search for any validation of the returned notebook ID
rg -n 'resolvedNotebookId|findCurrentNotebookId' src/notebooks/deepnote/deepnoteSerializer.ts -A 5 -B 2

Repository: deepnote/vscode-deepnote

Length of output: 1363


🏁 Script executed:

# Check if there are test cases validating when pending resolution is consumed but notebook doesn't exist
rg -n 'should.*pending.*not.*exist|invalid.*pending|notebook.*not.*found' src/notebooks/deepnote/deepnoteSerializer.unit.test.ts -i

Repository: deepnote/vscode-deepnote

Length of output: 50


🏁 Script executed:

# Check if there's a peek or non-destructive read method available
rg -n 'peek|getNot|getValidPending' src/notebooks/deepnote/deepnoteNotebookManager.ts -A 3

Repository: deepnote/vscode-deepnote

Length of output: 780


🏁 Script executed:

# Check what happens when selectedNotebook is undefined and whether there's any retry
sed -n '107,125p' src/notebooks/deepnote/deepnoteSerializer.ts

Repository: deepnote/vscode-deepnote

Length of output: 946


Consider peeking at pending resolution before consuming to avoid losing the hint on validation failure.

consumePendingNotebookResolution destructively removes from the queue before validation. While validation happens immediately after (line 107-109) and errors are thrown if the notebook isn't found, a peek-first approach would be more robust: check existence via find() before actually consuming. This prevents the hint from being lost if any future retry mechanism or validation pathway needs it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notebooks/deepnote/deepnoteSerializer.ts` around lines 196 - 214, The
code currently calls notebookManager.consumePendingNotebookResolution(projectId)
which removes the pending hint before validation; change this to first peek
non-destructively (e.g. notebookManager.peekPendingNotebookResolution(projectId)
or notebookManager.getPendingNotebookResolution(projectId)) to obtain
pendingNotebookId, then perform the existence/validation checks and only call
notebookManager.consumePendingNotebookResolution(projectId) to remove it after
the notebook is confirmed valid; update findCurrentNotebookId to use peek/get
first and consume only on successful validation so the hint isn't lost on
validation failure.

Comment on lines +558 to +572
return [
...workspace.notebookDocuments.reduce((ids, doc) => {
if (doc.notebookType !== 'deepnote' || doc.metadata.deepnoteProjectId !== projectId) {
return ids;
}

const parsed = z.string().safeParse(doc.metadata.deepnoteNotebookId);

if (parsed.success) {
ids.add(parsed.data);
}

return ids;
}, new Set<string>())
];
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

Spread from Set is fine; minor readability nit.

Consider direct Array.from() instead of spread for clarity:

Optional simplification
-        return [
-            ...workspace.notebookDocuments.reduce((ids, doc) => {
+        return Array.from(workspace.notebookDocuments.reduce((ids, doc) => {
             ...
-            }, new Set<string>())
-        ];
+            }, new Set<string>()));
📝 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
return [
...workspace.notebookDocuments.reduce((ids, doc) => {
if (doc.notebookType !== 'deepnote' || doc.metadata.deepnoteProjectId !== projectId) {
return ids;
}
const parsed = z.string().safeParse(doc.metadata.deepnoteNotebookId);
if (parsed.success) {
ids.add(parsed.data);
}
return ids;
}, new Set<string>())
];
return Array.from(workspace.notebookDocuments.reduce((ids, doc) => {
if (doc.notebookType !== 'deepnote' || doc.metadata.deepnoteProjectId !== projectId) {
return ids;
}
const parsed = z.string().safeParse(doc.metadata.deepnoteNotebookId);
if (parsed.success) {
ids.add(parsed.data);
}
return ids;
}, new Set<string>()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/notebooks/deepnote/deepnoteSerializer.ts` around lines 558 - 572, Return
currently spreads a Set created by workspace.notebookDocuments.reduce into an
array; for clarity replace the final spread with Array.from to convert the Set
to an array. Locate the reducer that iterates over workspace.notebookDocuments
(checking doc.notebookType, doc.metadata.deepnoteProjectId, and parsing
doc.metadata.deepnoteNotebookId) and change the return from [...<Set>] to
Array.from(<Set>) so the intent is clearer while keeping the same logic.

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.

2 participants