Skip to content

refactor(notebook): daemon-owned notebook loading — Phase 4 Tauri client#602

Merged
rgbkrk merged 11 commits intomainfrom
daemon-owned-notebook-loading
Mar 8, 2026
Merged

refactor(notebook): daemon-owned notebook loading — Phase 4 Tauri client#602
rgbkrk merged 11 commits intomainfrom
daemon-owned-notebook-loading

Conversation

@rgbkrk
Copy link
Member

@rgbkrk rgbkrk commented Mar 8, 2026

Closes #598.

Switches all notebook creation/opening entry points from local NotebookState construction + relay population to daemon requests via connect_open_split/connect_create_split. Phases 1-3 and 5 were merged in PR #601; this PR completes Phases 4, 6, and 7.

What changed

Every entry point that creates or opens a notebook now delegates to the daemon instead of parsing .ipynb locally:

Entry point Before After
open_notebook_window load_notebook_state_for_path → parse .ipynb OpenMode::Open → daemon loads
spawn_new_notebook (Cmd-N) NotebookState::new_empty_with_runtime OpenMode::Create → daemon creates
complete_onboarding NotebookState::new_empty_with_runtime OpenMode::Create
run() startup (CLI/session) load_notebook_state_for_path or new_empty_with_runtime OpenMode::Open/Create/Restore
Session restore (additional windows) load_window_session_state → parse .ipynb OpenMode::Open/Create/Restore
save_notebook_as get_cells()FrontendCell conversion → carry cells initialize_notebook_sync_open (daemon reloads from disk)
macOS file-open handler load_notebook_state_for_path initialize_notebook_sync_open
reconnect_to_daemon old handshake with empty cells initialize_notebook_sync_open (saved) or old handshake (untitled)

What was deleted

  • notebook_state field from WindowNotebookContext
  • derive_notebook_id — daemon canonicalizes paths
  • create_notebook_window / create_notebook_window_with_label — replaced by _for_daemon
  • create_new_notebook_state — daemon detects project files
  • load_notebook_state_for_path — daemon loads .ipynb
  • create_window_context (old version) — replaced by _for_daemon
  • load_window_session_state from session.rs
  • Cell population loop in initialize_notebook_sync
  • FrontendCell import from lib.rs
  • get_cells() call in save_notebook_as

Architecture

Window creation is synchronous (appears immediately), daemon connection is async (spawned). The notebook_id starts as a placeholder and is updated when the daemon responds with the canonical ID. daemon:ready now carries a DaemonReadyPayload { notebook_id, cell_count, needs_trust_approval }.

Untitled notebook session restore uses OpenMode::Restore which reconnects via the legacy NotebookSync handshake with the env_id — the daemon may have the Automerge doc persisted from the previous session.

Test plan

New notebook creation

  • Cmd-N → new Python notebook appears, kernel auto-launches
  • Cmd-N → new Deno notebook appears, kernel auto-launches
  • Type in a cell, execute — output renders

Opening existing notebooks

  • File → Open → select .ipynb — cells and outputs load
  • [-] Open a trusted notebook with deps — kernel auto-launches
  • [-] Open an untrusted notebook with deps — shows trust approval prompt
  • [-] Open a large notebook (50+ cells) — all cells render

Save-as

  • Save-as to a new path — window title updates, kernel stays running
  • Execute a cell after save-as — output appears in the new room
  • [ ] Open the original path in a new window — old content still there We don't expose Save As as a base feature, it's just for untitled notebooks at the moment.

Multi-window

Skipped due to existing bug for multiwindow

  • [-] Open the same file in two windows — second joins existing room
  • [-] Edit in one window — change appears in the other
  • [-] Close one window — other keeps working

Session restore

  • Open 2-3 notebooks, quit app, reopen — all windows restore
  • Untitled notebook with edits, quit, reopen — content restored from daemon
  • Session with a deleted file — window opens without crashing (empty or error)

Onboarding

  • Fresh settings (delete ~/.config/runt/settings.json) → onboarding flow → first notebook created

macOS file associations

Skipping. Will test in Nightly release.

  • [-] Drag .ipynb onto dock icon — opens in empty main window or new window
  • [-] Double-click .ipynb in Finder — opens correctly

Reconnection

  • Stop daemon (runt daemon stop), wait for disconnect banner, restart daemon — reconnects
  • Saved notebook reconnect — cells reload from disk via daemon
  • Untitled notebook reconnect — content restored from daemon's persisted doc

Edge cases

  • Open notebook while daemon is starting up — loading state, then cells appear
  • Rapidly Cmd-N several times — each gets its own window and room

Known limitations (deferred)

Follow-ups

rgbkrk added 11 commits March 7, 2026 20:39
…ayload

Add initialize_notebook_sync_open and initialize_notebook_sync_create
alongside the existing initialize_notebook_sync. These use
connect_open_split/connect_create_split to delegate notebook loading
to the daemon instead of parsing .ipynb locally.

Extract setup_sync_receivers as the shared tail — stores the handle,
spawns the 3 relay tasks (metadata, raw sync, broadcast), and emits
daemon:ready with a DaemonReadyPayload containing notebook_id,
cell_count, and needs_trust_approval.

Add create_window_context_for_daemon which creates a WindowNotebookContext
without requiring a fully-parsed NotebookState. Uses a minimal stub
for transitional code that still reads notebook_state.

Add runtime field to WindowNotebookContext so session save can read
it directly instead of going through NotebookState.

No callers yet — entry points are converted in subsequent commits.
…ading

Switch open_notebook_window, spawn_new_notebook, and complete_onboarding
to use create_notebook_window_for_daemon with OpenMode::Open/Create.

- open_notebook_window: no longer calls load_notebook_state_for_path or
  parses .ipynb locally. Daemon loads the file.
- spawn_new_notebook: no longer calls NotebookState::new_empty_with_runtime.
  Daemon creates the empty notebook.
- complete_onboarding: same as spawn_new_notebook but with working_dir
  from ensure_notebooks_directory().

The old create_notebook_window/create_notebook_window_with_label are
still used by run() startup and session restore (converted next).
…on-owned loading

The main window and additional session windows now use daemon-owned
loading instead of local NotebookState construction:

- CLI path → OpenMode::Open (daemon loads .ipynb)
- Session with path → OpenMode::Open (daemon loads)
- Session untitled with env_id → OpenMode::Restore (reconnect to
  existing daemon room via old handshake)
- No session, no path → OpenMode::Create (daemon creates empty notebook)

Additional session windows use create_notebook_window_for_daemon
instead of create_notebook_window_with_label, avoiding
load_window_session_state and local .ipynb parsing.

The async daemon sync init block now dispatches to the appropriate
init function based on OpenMode instead of extracting cells/metadata
from NotebookState.

create_window_context (old version taking NotebookState) is now unused.
…oading

When a .ipynb file is opened via Finder/dock icon on an empty main window,
the handler now updates the context path and spawns
initialize_notebook_sync_open instead of calling load_notebook_state_for_path.

No local .ipynb parsing for the reuse-main-window case. The new-window
case already goes through open_notebook_window (converted earlier).
Replace the get_cells() → FrontendCell conversion → initialize_notebook_sync
chain with initialize_notebook_sync_open. The daemon just wrote the file,
so OpenNotebook loads it right back without carrying cells across.

This eliminates:
- The last get_cells() call site on NotebookSyncHandle
- The last FrontendCell usage in lib.rs
- The CellSnapshot → FrontendCell conversion code
- notebook_state.path sync-back (no longer needed)
- Local notebook_id derivation in save-as (daemon canonicalizes)

Dead code warnings now show: derive_notebook_id, notebook_state_for_window,
create_notebook_window, create_notebook_window_with_label, create_window_context
are all unused.
For saved notebooks, use initialize_notebook_sync_open (daemon reloads
from disk). For untitled notebooks, use the old handshake with the
notebook_id (env_id) so the daemon can find its persisted Automerge doc.
save_session now reads path from context.path, notebook_id from
context.notebook_id (env_id for untitled), and runtime from
context.runtime. No more locking NotebookState.

Remove load_window_session_state — session restore now dispatches
directly to OpenMode in run() without constructing NotebookState.
Remove all code that is no longer called now that every entry point
uses daemon-owned loading:

Deleted functions:
- derive_notebook_id (daemon canonicalizes paths)
- notebook_state_for_window (no code reads NotebookState)
- create_notebook_window / create_notebook_window_with_label (replaced by _for_daemon)
- create_new_notebook_state (daemon detects project files)
- load_notebook_state_for_path (daemon loads .ipynb)
- create_window_context (replaced by _for_daemon)

Removed from WindowNotebookContext:
- notebook_state field (session save reads Arc fields)

Simplified initialize_notebook_sync:
- Removed initial_cells and initial_metadata params (all callers passed empty)
- Removed cell population loop (daemon populates the room)
- Removed FrontendCell import (no longer used in lib.rs)

The NotebookState stub in create_window_context_for_daemon is also
gone — the context no longer holds any NotebookState.

Net: -268 lines. Zero warnings.
…_window_for_daemon

The placeholder_id was computed from path alone, which is None for
Restore and Create variants — resulting in an empty notebook_id in
the context. When reconnect_to_daemon later reads context.notebook_id,
it connected to a room with empty ID instead of the env_id.

Now derives placeholder_id from the OpenMode variant:
- Open: canonical path (same as before)
- Restore: notebook_id from the variant (the env_id)
- Create: empty (daemon generates UUID)

This fixes session restore for untitled notebooks showing as empty
on reconnect.
new_fresh previously deleted all persisted docs unconditionally,
treating .ipynb as the sole source of truth. For untitled notebooks
(UUID-based notebook_ids), the persisted Automerge doc is their only
content record — there's no .ipynb on disk.

Now: for untitled notebooks, load the persisted doc if it exists so
content survives daemon restarts. For saved notebooks, still delete
the stale persisted doc and load from .ipynb (unchanged behavior).

Add test verifying persisted docs are loaded for UUID notebook_ids.
rgbkrk pushed a commit that referenced this pull request Mar 8, 2026
Add findings from thorough analysis of all files:
- Inconsistent daemon:ready for Restore path
- Wrong runtime for opened notebooks
- Heartbeat monitor connection churn and single-failure sensitivity
- Empty notebook re-load issue
- Error-path stream drain timeout
- Silent poisoned mutex failures

https://claude.ai/code/session_01H93XZZFaeq3fayDSwSdGkB
rgbkrk pushed a commit that referenced this pull request Mar 8, 2026
Add iopub task leak, doc write lock during I/O, runtime validation,
and broadcast lag recovery gaps from daemon/kernel manager analysis.

https://claude.ai/code/session_01H93XZZFaeq3fayDSwSdGkB
@rgbkrk rgbkrk marked this pull request as ready for review March 8, 2026 05:57
@rgbkrk rgbkrk merged commit 2369698 into main Mar 8, 2026
14 checks passed
@rgbkrk rgbkrk deleted the daemon-owned-notebook-loading branch March 8, 2026 06:08
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.

refactor: daemon-owned notebook loading — eliminate NotebookState

1 participant