From c5d010e7d76a28026efbb4bb17d0f04a6a00d858 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 21 Oct 2026 00:02:01 -0700 Subject: [PATCH] MCP: document.inspect and document.open report the File Info title as fileInfoTitle The title field of document.inspect and the document.open reply is the document's tab name, which comes from the file name once the document is saved to or opened from a file. A client had to call file.info or document.info to read the File Info title, so reading a saved document's title from these replies gave the file name instead. Both replies now add fileInfoTitle, the title file.info reports, and keep title as it was. docs/mcp.md's File Info section describes both. --- crates/engine/src/cmd/fileio/load.rs | 8 ++++++-- crates/engine/src/inspect.rs | 2 ++ crates/engine/src/tests_fileinfo.rs | 23 +++++++++++++++++++++++ docs/mcp.md | 3 +++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index 302240baa..d7fe553be 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -463,8 +463,12 @@ fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option Value { json!({ "uid": st.uid, "title": st.title(), + // The File Info title (`file.info`), where `title` comes from the file name once the document has one. + "fileInfoTitle": d.title, "path": st.path, "dirty": st.is_dirty(), "revision": st.revision, diff --git a/crates/engine/src/tests_fileinfo.rs b/crates/engine/src/tests_fileinfo.rs index 09d1efd17..bbe8eca66 100644 --- a/crates/engine/src/tests_fileinfo.rs +++ b/crates/engine/src/tests_fileinfo.rs @@ -112,6 +112,29 @@ fn saving_to_a_file_stamps_the_modified_date() { assert!(s.execute("file.info", &json!({})).unwrap()["modified"].is_string()); } +#[test] +fn inspect_and_open_report_the_file_info_title_beside_the_file_name() { + let dir = std::env::temp_dir().join(format!("vc-fileinfotitle-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("poster.vectorcraft"); + let mut s = session(); + // Untitled, the two are the same. + let r = s.execute("document.inspect", &json!({"depth": 0})).unwrap(); + assert_eq!((r["title"].as_str(), r["fileInfoTitle"].as_str()), (Some("Poster"), Some("Poster"))); + s.execute("file.info", &json!({"title": "Poster: final"})).unwrap(); + s.execute("document.save", &json!({"path": path.to_str().unwrap()})).unwrap(); + let r = s.execute("document.inspect", &json!({"depth": 0})).unwrap(); + assert_eq!((r["title"].as_str(), r["fileInfoTitle"].as_str()), (Some("poster.vectorcraft"), Some("Poster: final"))); + // Opened from the file, in another session. + let mut s = Session::new(); + let opened = s.execute("document.open", &json!({"path": path.to_str().unwrap()})).unwrap(); + assert_eq!((opened["title"].as_str(), opened["fileInfoTitle"].as_str()), (Some("poster.vectorcraft"), Some("Poster: final"))); + let r = s.execute("document.inspect", &json!({"depth": 0})).unwrap(); + assert_eq!((r["title"].as_str(), r["fileInfoTitle"].as_str()), (Some("poster.vectorcraft"), Some("Poster: final"))); + assert_eq!(r["fileInfoTitle"], s.execute("file.info", &json!({})).unwrap()["title"]); + let _ = std::fs::remove_dir_all(dir); +} + #[test] fn pdf_info_carries_author_subject_and_keywords() { let mut s = session(); diff --git a/docs/mcp.md b/docs/mcp.md index d5fb60d25..ae4eb5778 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3184,6 +3184,9 @@ packet (`dc:creator`, `dc:description`, `dc:subject` or `pdf:Keywords`, `xmpRigh `Subject`, `Keywords`, `CreationDate`, `ModDate`) give, the XMP packet's value where both give one. A value that `file.info` would refuse is left out, and the title is set as for any other open (the file name, or a new untitled name for a template). +`document.inspect` (`inspect_document`) and the `document.open` (`open_file`) reply report the File Info title as +`fileInfoTitle`. Their `title` is the document's tab name, which comes from the file name once the document is saved +to or opened from a file (with ` [Converted]` or ` [Recovered]` where that applies). `file.new {created}` and a save to a file (`document.save` or `file.saveAs` with `{path, modified}`) use the given date (Unix seconds, or `null` for none) instead of now. Their journal entries always record the date they used, so replaying the journal makes the same document whenever it runs (an action recorded in