Skip to content

feat(runtimed): create new notebook when opening non-existent path#685

Merged
rgbkrk merged 1 commit intomainfrom
quill/new-notebook-focus
Mar 10, 2026
Merged

feat(runtimed): create new notebook when opening non-existent path#685
rgbkrk merged 1 commit intomainfrom
quill/new-notebook-focus

Conversation

@rgbkrk
Copy link
Member

@rgbkrk rgbkrk commented Mar 10, 2026

Summary

When runt notebook newfile.ipynb is called with a path that doesn't exist, the daemon now creates a new empty notebook at that path instead of failing. This matches the expected behavior of CLI tools like vim.

Changes

  • Check file existence before canonicalizing path
  • For non-existent files: use absolute path for notebook_id, create empty notebook in doc
  • For permission/IO errors: return error to client instead of silently failing

Verification

  • Start dev daemon: cargo xtask dev-daemon
  • Run ./target/debug/runt notebook /tmp/test-new.ipynb
  • Verify notebook opens with one empty cell
  • Save the notebook and verify /tmp/test-new.ipynb is created on disk
  • Close and reopen the notebook to verify saved content loads
  • Test error case: ./target/debug/runt notebook /root/nopermission.ipynb returns error

PR submitted by @rgbkrk's agent, Quill

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the OpenNotebook handshake in the runtimed daemon to treat a non-existent .ipynb path as “create a new notebook here” rather than failing, aligning CLI behavior with typical editors.

Changes:

  • Checks file existence before attempting to canonicalize the provided path.
  • For missing files, derives notebook_id from an absolute path and initializes the room doc with a new empty notebook.
  • For permission/IO errors during access checks, returns a structured error response to the client.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +1057 to +1064
crate::notebook_sync_server::create_empty_notebook(
&mut doc,
&default_runtime.to_string(),
default_python_env.clone(),
Some(&notebook_id),
)
.map_err(|e| anyhow::anyhow!(e))?;
info!("[runtimed] Created new notebook at {}", path);
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

If create_empty_notebook fails, this function returns early with an error before sending NotebookConnectionInfo, which may leave the client waiting on a response frame. Consider mirroring the structured error-response pattern used above (and in handle_create_notebook) so OpenNotebook failures on the new-notebook path still return a NotebookConnectionInfo { error: ... } to the client.

Suggested change
crate::notebook_sync_server::create_empty_notebook(
&mut doc,
&default_runtime.to_string(),
default_python_env.clone(),
Some(&notebook_id),
)
.map_err(|e| anyhow::anyhow!(e))?;
info!("[runtimed] Created new notebook at {}", path);
let result = crate::notebook_sync_server::create_empty_notebook(
&mut doc,
&default_runtime.to_string(),
default_python_env.clone(),
Some(&notebook_id),
);
match result {
Ok(()) => {
info!("[runtimed] Created new notebook at {}", path);
}
Err(e) => {
error!(
"[runtimed] Failed to create new notebook at {}: {}",
path, e
);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines 1051 to +1067
// Check whether this connection needs to stream-load the notebook
// from disk. The actual load is deferred to the sync loop so cells
// arrive progressively via Automerge sync messages.
let (cell_count, needs_load) = {
// from disk, or create a new empty notebook.
let (cell_count, needs_load) = if !file_exists {
// File doesn't exist - create empty notebook in the doc
let mut doc = room.doc.write().await;
if doc.cell_count() == 0 {
crate::notebook_sync_server::create_empty_notebook(
&mut doc,
&default_runtime.to_string(),
default_python_env.clone(),
Some(&notebook_id),
)
.map_err(|e| anyhow::anyhow!(e))?;
info!("[runtimed] Created new notebook at {}", path);
}
(doc.cell_count(), None) // No streaming load needed
} else {
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

Creating an empty notebook for a non-existent file path still uses a path-based notebook_id (not a UUID). In handle_notebook_sync_connection, auto-launch is gated by room.notebook_path.exists() || is_new_notebook where is_new_notebook only becomes true for UUID notebook_ids; for a newly-created (non-existent) file path this will evaluate false and prevent auto-launch even though the doc now has content. Consider adjusting the auto-launch gating for this case (e.g., treat “created empty notebook” as new, or relax the is_new_notebook check when needs_load is None and the doc was just initialized).

Copilot uses AI. Check for mistakes.
Comment on lines +1011 to +1014
send_json_frame(&mut writer, &response).await?;
// Drain any remaining data from client before closing
let _ = tokio::io::copy(&mut reader, &mut tokio::io::sink()).await;
return Ok(());
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

tokio::io::copy(&mut reader, &mut tokio::io::sink()).await will block until the client closes its write-half; if the client waits for the server to close first, this can hang the task indefinitely. Consider removing the drain or wrapping it in a short timeout / bounded read, and/or calling shutdown() on the writer after sending the error frame.

Copilot uses AI. Check for mistakes.
notebook_id: String::new(),
cell_count: 0,
needs_trust_approval: false,
error: Some(format!("Cannot access notebook: {}", e)),
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The error response sent back to the client omits the notebook path, which makes it harder for users to understand which open failed (especially if multiple windows/requests are involved). Consider including the path in the error string (or a dedicated field, if available) so the client can surface a more actionable message.

Suggested change
error: Some(format!("Cannot access notebook: {}", e)),
error: Some(format!("Cannot access notebook '{}': {}", path, e)),

Copilot uses AI. Check for mistakes.
Comment on lines +1022 to +1026
path_buf
.canonicalize()
.unwrap_or_else(|_| path_buf.clone())
.to_string_lossy()
.to_string()
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

For existing files, canonicalize() failures are still silently swallowed via unwrap_or_else(|_| path_buf.clone()). This can produce a non-canonical notebook_id even when the file exists (e.g., due to permission/symlink issues), undermining the stated goal of returning permission/IO errors to the client and potentially creating duplicate rooms for the same file. Consider handling canonicalize errors explicitly (e.g., treat as an error to the client, or fall back to absolute() only for specific cases).

Suggested change
path_buf
.canonicalize()
.unwrap_or_else(|_| path_buf.clone())
.to_string_lossy()
.to_string()
match path_buf.canonicalize() {
Ok(canonical) => canonical.to_string_lossy().to_string(),
Err(e) => {
// Permission denied, I/O error, or other issue while canonicalizing
// Treat this as an error rather than silently falling back
let (mut reader, mut writer) = tokio::io::split(stream);
let response = NotebookConnectionInfo {
protocol: PROTOCOL_V2.to_string(),
protocol_version: Some(PROTOCOL_VERSION),
daemon_version: Some(crate::daemon_version().to_string()),
notebook_id: String::new(),
cell_count: 0,
needs_trust_approval: false,
error: Some(format!("Cannot canonicalize notebook path: {}", e)),
};
send_json_frame(&mut writer, &response).await?;
// Drain any remaining data from client before closing
let _ = tokio::io::copy(&mut reader, &mut tokio::io::sink()).await;
return Ok(());
}
}

Copilot uses AI. Check for mistakes.
@rgbkrk rgbkrk force-pushed the quill/new-notebook-focus branch from a6d3aa2 to 51a6424 Compare March 10, 2026 21:58
@rgbkrk
Copy link
Member Author

rgbkrk commented Mar 10, 2026

Addressed Copilot Review Comments

  1. Include path in error message - Error messages now include the notebook path for better debugging: Cannot access notebook '{}': {}

  2. Handle canonicalize failures explicitly - Canonicalize errors for existing files now return proper error responses instead of silently falling back

  3. Remove drain that could hang - Removed the tokio::io::copy drain that could hang indefinitely if the client waits for server to close first

  4. Send error response on create_empty_notebook failure - Now sends proper NotebookConnectionInfo { error: ... } response instead of returning early

  5. Fix auto-launch for new notebooks at path - Added created_new_at_path parameter to handle_notebook_sync_connection so auto-launch works for notebooks created via runt notebook newfile.ipynb

@rgbkrk rgbkrk force-pushed the quill/new-notebook-focus branch from 51a6424 to 8a8fc8d Compare March 10, 2026 22:25
@rgbkrk rgbkrk enabled auto-merge (squash) March 10, 2026 22:31
@rgbkrk rgbkrk marked this pull request as draft March 10, 2026 22:36
auto-merge was automatically disabled March 10, 2026 22:36

Pull request was converted to draft

When `runt notebook newfile.ipynb` is called with a path that doesn't
exist, the daemon now creates a new empty notebook at that path instead
of failing. This matches the expected behavior of CLI tools like vim.

- Check file existence before canonicalizing path
- For non-existent files: use absolute path for notebook_id, create
  empty notebook in doc
- For permission/IO errors: return error to client instead of silently
  failing
@rgbkrk rgbkrk force-pushed the quill/new-notebook-focus branch from 8a8fc8d to 6585919 Compare March 10, 2026 22:44
@rgbkrk rgbkrk marked this pull request as ready for review March 10, 2026 22:57
@rgbkrk rgbkrk merged commit a42397f into main Mar 10, 2026
14 checks passed
@rgbkrk rgbkrk deleted the quill/new-notebook-focus branch March 10, 2026 23:23
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