feat(engine): rename documents from the title bar and open account settings from the badge (#1016)

* M10: enable document renaming and account settings

* no-mistakes(review): Allow case-only document renames without overwriting files

* no-mistakes(review): Keep unsaved rename as session file name, not Title

* no-mistakes(review): Keep AutoSave and Recent across renames; Compare clears name

* no-mistakes(document): Document file.rename write-root rule; dedupe rename gap note

* vi: Rename Document strings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Prashant Neupane
2026-10-14 15:30:52 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 5597351213
commit aedbbc9325
32 changed files with 424 additions and 16 deletions
+3 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: All Caps text in PDFs, #1473; previously trivial: progress log: AutoFit in a nested table, whole cells shaded for a selection through a table, Shift+Right at a cell's end, #971; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (title rename and account settings; previously trivial: progress log: All Caps text in PDFs, #1473; previously trivial: progress log: AutoFit in a nested table, whole cells shaded for a selection through a table, Shift+Right at a cell's end, #971; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
@@ -136,6 +136,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| Date | What landed |
|---|---|
| 2026-10-12 | Document title opens Rename Document; the account badge opens Options › General for user name and initials. `file.rename {name}` preserves file extensions and refuses an existing destination; unsaved names become the suggested save name without changing the Title property. |
| 2026-10-13 | Rounded rectangles (#1476): a `roundRect` keeps the corner radius its file gives it (`adj`), on screen, in PDF and when saved; all were drawn with a 16% corner |
| 2026-10-13 | PAGEREF in headers and footers (#1477): "Page X of Y" with Y a page reference to a bookmark at each letter's end shows each letter's own total instead of the result stored in the file |
| 2026-10-13 | Replace All respects locked content controls with Track Changes on or off; it refuses the entire batch before editing when any match would change locked content or remove a control locked against deletion |
@@ -327,6 +328,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | trivial | Document title opens Rename Document; the account badge opens Options › General for user name and initials. |
| 2026-10-13 | trivial | Progress log: rounded rectangle corners (#1476) |
| 2026-10-13 | trivial | Progress log: PAGEREF in headers and footers (#1477) |
| 2026-10-13 | trivial | Replace All checks content-control locks before editing; a refused batch leaves the document and undo history unchanged; existing deletion-lock rules reused, with tracked/untracked regression coverage |
+3
View File
@@ -75,6 +75,9 @@ sync_github_name = false
real_name = "Manuel Carmona"
display_name = "manucarcor"
[people.odedigo]
[people.neupaneprashant]
real_name = "Prashant Neupane"
[people.cyril265]
display_name = "cyril265"
+31
View File
@@ -31,8 +31,39 @@ pub(crate) fn set_author(s: &mut Session, v: &Value) -> CmdResult {
Ok(r)
}
/// Rename the current file in its directory, or name a document before its first save.
fn rename(s: &mut Session, v: &Value) -> CmdResult {
let name = p::req_str(v, "name")?.trim();
if name.is_empty()
|| name.chars().count() > 200
|| name.ends_with(['.', ' '])
|| name.chars().any(|c| c.is_control() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|'))
|| matches!(name, "." | "..")
{
return Err(CmdError::Params("Enter a valid document name (up to 200 characters, without path separators).".into()));
}
if let Some(path) = &s.path {
let mut destination = path.with_file_name(name);
// The field edits the stem; retain the original format, including macro/template files.
if let Some(ext) = path.extension() {
destination.set_file_name(format!("{name}.{}", ext.to_string_lossy()));
}
if destination != *path {
#[cfg(not(target_arch = "wasm32"))]
{
crate::io::rename_file(path, &destination).map_err(CmdError::Failed)?;
}
s.path = Some(destination);
}
} else {
s.file_name = Some(name.to_string());
}
Ok(json!({"name": name, "path": s.path}))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("file.rename", "Rename Document", "File › Info", rename).params(r#"{"name": string (document name without its extension)}"#).pure(),
CommandSpec::new("file.new", "New", "File", new).key("Mod+N").params(r#"{"template"?: "blank|sample|letter|resume|report"}"#).pure(),
CommandSpec::new("file.open", "Open", "File", open)
.key("Mod+O / Mod+F12")
+1
View File
@@ -1114,6 +1114,7 @@ fn compare(s: &mut Session, v: &Value) -> CmdResult {
if show_in != "original" || p::str(v, "original").is_some() {
// The result is a new, untitled document: saving it never overwrites either file.
s.path = None;
s.file_name = None;
}
s.sel = Selection::caret(s.doc.start_of(StoryRef::Body));
Ok(json!({"changes": changes, "author": author, "notHonoured": not_honoured}))
+34
View File
@@ -449,6 +449,40 @@ pub fn read_file_capped(path: &str, max: u64, what: &str) -> Result<Vec<u8>, Str
std::fs::read(path).map_err(|e| format!("{path}: {e}"))
}
/// Rename without replacing another file, respecting both automation access roots.
#[cfg(not(target_arch = "wasm32"))]
pub fn rename_file(source: &std::path::Path, destination: &std::path::Path) -> Result<(), String> {
crate::file_access::check_read(&source.to_string_lossy())?;
crate::file_access::check_write(&source.to_string_lossy())?;
crate::file_access::check_write(&destination.to_string_lossy())?;
// Creating a sibling hard link refuses collisions atomically and keeps the original bytes.
if let Err(e) = std::fs::hard_link(source, destination) {
if e.kind() == std::io::ErrorKind::AlreadyExists && only_case_differs(source, destination) {
return std::fs::rename(source, destination).map_err(|e| format!("Could not rename document: {e}"));
}
return Err(format!("Could not rename document: {e}"));
}
if let Err(e) = std::fs::remove_file(source) {
let _ = std::fs::remove_file(destination);
return Err(format!("Could not rename document: {e}"));
}
Ok(())
}
/// Whether `destination` names `source` in the same directory spelled with different case, and
/// the directory holds no entry spelled exactly as `destination`.
#[cfg(not(target_arch = "wasm32"))]
fn only_case_differs(source: &std::path::Path, destination: &std::path::Path) -> bool {
let (Some(dir), Some(from), Some(to)) = (source.parent(), source.file_name(), destination.file_name()) else {
return false;
};
if destination.parent() != Some(dir) || from == to || from.to_string_lossy().to_lowercase() != to.to_string_lossy().to_lowercase() {
return false;
}
let dir = if dir.as_os_str().is_empty() { std::path::Path::new(".") } else { dir };
std::fs::read_dir(dir).is_ok_and(|entries| entries.filter_map(Result::ok).all(|entry| entry.file_name() != to))
}
/// The first `max` bytes of the file at `path` (fewer when it is shorter), inside the automation
/// read root when there is one: enough to tell a file's format by its start.
#[cfg(not(target_arch = "wasm32"))]
+6
View File
@@ -337,6 +337,9 @@ pub struct Session {
/// Formatting picked with a collapsed caret, applied to the next typed text.
pub pending: Option<CharProps>,
pub path: Option<std::path::PathBuf>,
/// Name given to a document not saved yet (File › Info › Rename): what Save suggests. A file
/// name, not document metadata, so it is not part of the document or its undo history.
pub file_name: Option<String>,
pub dirty: bool,
/// Where the custom dictionary is kept on disk, if the host wired one up. Only the desktop
/// app does: headless sessions (MCP, `wordcraft-cli run`), tests and the web build leave
@@ -586,6 +589,7 @@ impl Session {
view: ViewState::default(),
pending: None,
path: None,
file_name: None,
user_dict_path: None,
dirty: false,
clipboard: None,
@@ -985,6 +989,7 @@ impl Session {
self.doc = doc;
self.doc.ensure_nonempty();
self.password = None;
self.file_name = None;
self.sel = Selection::caret(self.doc.start_of(StoryRef::Body));
self.pending = None;
self.also_selected.clear();
@@ -1024,6 +1029,7 @@ impl Session {
view: _,
pending: _,
path: _,
file_name: _,
// The custom dictionary file, like the file path: host wiring, not an edit.
user_dict_path: _,
dirty,
+62
View File
@@ -5541,6 +5541,68 @@ fn insert_picture_takes_a_windows_metafile() {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn rename_document_preserves_saved_contents_and_rejects_collisions() {
let dir = std::env::temp_dir().join(format!("wordcraft-rename-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let original = dir.join("original.docm");
let renamed = dir.join("Notes.docm");
let existing = dir.join("existing.docm");
std::fs::write(&original, b"original bytes").unwrap();
std::fs::write(&existing, b"keep these bytes").unwrap();
let mut s = Session::new(wordcraft_doc::Document::new());
s.path = Some(original.clone());
s.dirty = true;
assert!(s.run("file.rename", &json!({"name": "existing"})).is_err());
assert_eq!(s.path.as_ref(), Some(&original));
assert_eq!(std::fs::read(&existing).unwrap(), b"keep these bytes");
let roots = crate::file_access::AutomationRoots::new(Some(&dir), None).unwrap();
assert!(crate::file_access::confine(roots.as_ref(), || s.run("file.rename", &json!({"name": "Notes"}))).is_err());
assert!(original.exists());
assert!(!renamed.exists());
s.run("file.rename", &json!({"name": " Notes "})).unwrap();
assert_eq!(s.path.as_ref(), Some(&renamed));
assert!(s.dirty);
assert!(!original.exists());
assert_eq!(std::fs::read(&renamed).unwrap(), b"original bytes");
s.run("file.rename", &json!({"name": "Notes"})).unwrap();
s.run("file.rename", &json!({"name": "notes"})).unwrap();
let lower = dir.join("notes.docm");
assert_eq!(s.path.as_ref(), Some(&lower));
let names: Vec<_> = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().file_name()).collect();
assert!(names.contains(&"notes.docm".into()) && !names.contains(&"Notes.docm".into()), "{names:?}");
assert_eq!(std::fs::read(&lower).unwrap(), b"original bytes");
assert!(s.run("file.rename", &json!({"name": "existing"})).is_err());
assert_eq!(s.path.as_ref(), Some(&lower));
assert_eq!(std::fs::read(&existing).unwrap(), b"keep these bytes");
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn rename_unsaved_document_names_the_file_not_the_document() {
let mut s = Session::new(wordcraft_doc::Document::new());
s.doc.core.title = "Quarterly report".into();
for name in ["", " ", "../report", "a/b", "a\\b", "a\nb", "name."] {
assert!(s.run("file.rename", &json!({"name": name})).is_err(), "{name:?}");
assert!(s.file_name.is_none());
}
s.run("file.rename", &json!({"name": " Résumé notes "})).unwrap();
assert_eq!(s.file_name.as_deref(), Some("Résumé notes"));
assert_eq!(s.doc.core.title, "Quarterly report");
assert!(s.path.is_none());
assert!(!s.dirty);
assert!(!s.can_undo());
s.run("file.new", &json!({})).unwrap();
assert!(s.file_name.is_none());
s.run("file.rename", &json!({"name": "Draft"})).unwrap();
s.run("file.open", &json!({"path": "other.txt", "data": "aGk="})).unwrap();
assert!(s.file_name.is_none());
s.run("file.new", &json!({})).unwrap();
s.run("file.rename", &json!({"name": "Draft"})).unwrap();
s.run("review.compare", &json!({"text": "changed", "showIn": "new"})).unwrap();
assert!(s.file_name.is_none() && s.path.is_none());
}
/// #1344: File › Info › Inspect Document finds hidden text by its effective formatting (direct,
/// character style, paragraph style) in every story — body, header, footer, footnote, comment,
/// text box — and Remove All deletes exactly that text, in one undo step, through a DOCX save.
+22 -4
View File
@@ -286,9 +286,12 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
// Account / community.
let (r, resp) = ui.allocate_exact_size(vec2(control_h, control_h), Sense::click());
ui.painter().circle_filled(r.center(), control_h / 2.0 - 1.0, t.accent);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!("Account")));
let initials: String = app.session.user_initials().chars().take(2).collect();
ui.painter().text(r.center(), Align2::CENTER_CENTER, initials, semibold(10.5), t.on_accent);
resp.on_hover_text(format!("{} — set your name in File › Options", app.session.author));
if resp.on_hover_text(format!("{} — set your name in File › Options", app.session.author)).clicked() {
let _ = app.run("ui.backstage", json!({"value": true, "page": "options", "pane": "general"}));
}
ui.add_space(8.0);
if app.ui.show_discord {
let (r, resp) = ui.allocate_exact_size(vec2(84.0, 22.0), Sense::click());
@@ -360,14 +363,29 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
};
let font = TypeRung::Chrome.semibold();
let track = TypeRung::Chrome.tracking();
let w = text_width(ui.painter(), &title, &font, track, t.text);
let cx = full.center().x;
if cx - w / 2.0 > qat_end + 10.0 && cx + w / 2.0 < right_start - 10.0 {
let available = 2.0 * (cx - qat_end - 10.0).min(right_start - 10.0 - cx);
let mut shown: String = title.chars().take(240).collect();
if text_width(ui.painter(), &shown, &font, track, t.text) > available {
while !shown.is_empty() && text_width(ui.painter(), &format!("{shown}…"), &font, track, t.text) > available {
shown.pop();
}
shown.push('…');
}
let w = text_width(ui.painter(), &shown, &font, track, t.text);
if available > 24.0 {
let rect = Rect::from_center_size(pos2(cx, full.center().y), vec2(w + 8.0, control_h));
let response = ui.interact(rect, ui.id().with("document_name"), Sense::click()).on_hover_text(tl!("Rename Document"));
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &title));
if response.clicked() {
let _ = app.run("file.rename", json!({}));
}
paint_text(
ui.painter(),
pos2(cx - w / 2.0, full.center().y - TypeRung::Chrome.line_height() / 2.0),
Align2::LEFT_TOP,
&title,
&shown,
&font,
track,
t.text,
+34
View File
@@ -140,6 +140,11 @@ fn special_menu(ui: &mut Ui, extra: &FindExtra) -> Option<&'static str> {
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "dialog", rename_all = "camelCase")]
pub enum Dialog {
RenameDocument {
name: String,
message: String,
focus: bool,
},
Font {
/// The font (Latin text) and the Asian text font, as the font lists name them, and their
/// values when the dialog opened: OK changes only the ones changed (#505).
@@ -879,6 +884,7 @@ pub(crate) fn color_field(ui: &mut Ui, theme: &[wordcraft_doc::Rgb], value: &mut
impl Dialog {
pub fn name(&self) -> &'static str {
match self {
Dialog::RenameDocument { .. } => "renameDocument",
Dialog::Font { .. } => "font",
Dialog::Paragraph { .. } => "paragraph",
Dialog::Tabs { .. } => "tabs",
@@ -1149,6 +1155,7 @@ impl Dialog {
}
}
"pasteSpecial" => Dialog::paste_special(app, &json!({})),
"renameDocument" => Dialog::RenameDocument { name: app.title_stem(), message: String::new(), focus: true },
"about" => Dialog::About { tab: 0 },
"contributors" => Dialog::About { tab: 1 },
"models" => Dialog::About { tab: 2 },
@@ -1335,6 +1342,7 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) {
let mut open = true;
let mut close = false;
let title = match &d {
Dialog::RenameDocument { .. } => "Rename Document",
Dialog::Font { .. } => "Font",
Dialog::Paragraph { .. } => "Paragraph",
Dialog::Tabs { .. } => "Tabs",
@@ -1607,6 +1615,32 @@ fn apply_font(app: &mut WordApp, d: &Dialog) {
/// Returns true to close.
fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
match d {
Dialog::RenameDocument { name, message, focus } => {
ui.label(tl!("Document name"));
let r = ui.add(egui::TextEdit::singleline(name).desired_width(320.0).char_limit(200));
if *focus {
r.request_focus();
*focus = false;
}
if !message.is_empty() {
ui.colored_label(ui.visuals().error_fg_color, message.as_str());
}
let (ok, cancel) = buttons(ui, tl!("Rename"));
if cancel {
return true;
}
if ok && name.trim() == app.title_stem() {
return true;
}
if ok {
match app.run("file.rename", json!({"name": name})) {
Ok(_) => return true,
Err(e) => *message = e,
}
}
false
}
Dialog::Font { font, font_ea, asian, size, bold, italic, underline, strike, sup, sub, small_caps, caps, hidden, color, spacing, .. } => {
// Build the sample text format for the preview pane below.
let preview_size = size.trim().parse::<f32>().unwrap_or(12.0).clamp(6.0, 72.0);
+6
View File
@@ -2462,6 +2462,12 @@
Table Properties… Свойства на таблица…
Search Търсене
Title Bar Заглавна лента
# Document naming and account controls.
Rename Document Преименуване на документ
Document name Име на документа
Rename Преименуване
Account Акаунт
Privacy Options Настройки за поверителност
Trust Center Център за сигурност
Privacy for this document Поверителност на този документ
Can't render this file because it contains an unexpected character in line 7 and column 25.
+6
View File
@@ -2087,6 +2087,12 @@
Automatic ({region}) Automaticky ({region})
Search Hledat
Title Bar Záhlaví okna
# Document naming and account controls.
Rename Document Přejmenovat dokument
Document name Název dokumentu
Rename Přejmenovat
Account Účet
Privacy Options Možnosti ochrany osobních údajů
Trust Center Centrum zabezpečení
Privacy for this document Soukromí tohoto dokumentu
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2081,6 +2081,12 @@
Automatic ({region}) Automatisch ({region})
Search Suchen
Title Bar Titelleiste
# Document naming and account controls.
Rename Document Dokument umbenennen
Document name Dokumentname
Rename Umbenennen
Account Konto
Privacy Options Datenschutzoptionen
Trust Center Sicherheitscenter
Privacy for this document Datenschutz für dieses Dokument
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2448,6 +2448,12 @@ preview Title Título
Start-up Inicio de la aplicación
Show the Start screen when WordCraft starts Mostrar la pantalla de inicio al abrir WordCraft
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Activado: WordCraft se abre en Archivo › Inicio, con un documento en blanco, las plantillas y tus documentos recientes. Desactivado: abre un documento en blanco.
# Document naming and account controls.
Rename Document Cambiar nombre del documento
Document name Nombre del documento
Rename Cambiar nombre
Account Cuenta
Privacy Options Opciones de privacidad
Trust Center Centro de confianza
Privacy for this document Privacidad de este documento
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2116,6 +2116,12 @@
Start-up Käivitamine
Show the Start screen when WordCraft starts Näita WordCrafti käivitamisel avakuva
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Sees: WordCraft avaneb vaates Fail › Avaleht, kus on tühi dokument, mallid ja hiljutised dokumendid. Väljas: avaneb tühi dokument.
# Document naming and account controls.
Rename Document Nimeta dokument ümber
Document name Dokumendi nimi
Rename Nimeta ümber
Account Konto
Privacy Options Privaatsussuvandid
Trust Center Usalduskeskus
Privacy for this document Selle dokumendi privaatsus
Can't render this file because it contains an unexpected character in line 459 and column 13.
+6
View File
@@ -2090,6 +2090,12 @@
Table Properties… Propriétés du tableau…
Search Rechercher
Title Bar Barre de titre
# Document naming and account controls.
Rename Document Renommer le document
Document name Nom du document
Rename Renommer
Account Compte
Privacy Options Options de confidentialité
Trust Center Centre de gestion de la confidentialité
Privacy for this document Confidentialité de ce document
Can't render this file because it contains an unexpected character in line 7 and column 25.
+6
View File
@@ -2470,6 +2470,12 @@
Insert Caption… Felirat beszúrása…
Table Properties… Táblázat tulajdonságai…
Search Keresés
# Document naming and account controls.
Rename Document Dokumentum átnevezése
Document name Dokumentum neve
Rename Átnevezés
Account Fiók
Privacy Options Adatvédelmi beállítások
Trust Center Adatvédelmi központ
Privacy for this document A dokumentum adatvédelme
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2457,6 +2457,12 @@ preview Title Esempio
Automatic ({region}) Automatico ({region})
Search Cerca
Title Bar Barra del titolo
# Document naming and account controls.
Rename Document Rinomina documento
Document name Nome del documento
Rename Rinomina
Account Account
Privacy Options Opzioni di privacy
Trust Center Centro protezione
Privacy for this document Privacy di questo documento
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2082,6 +2082,12 @@
Start-up 起動
Show the Start screen when WordCraft starts WordCraft の起動時にスタート画面を表示する
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. オン: WordCraft は [ファイル] › [ホーム] で開き、白紙の文書、テンプレート、最近使った文書を表示します。オフ: 白紙の文書を開きます。
# Document naming and account controls.
Rename Document ドキュメント名の変更
Document name ドキュメント名
Rename 名前の変更
Account アカウント
Privacy Options プライバシー オプション
Trust Center セキュリティ センター
Privacy for this document このドキュメントのプライバシー
Can't render this file because it contains an unexpected character in line 566 and column 13.
+6
View File
@@ -2466,6 +2466,12 @@ preview Title Tytuł
Typing Pisanie
Search Wyszukaj
Title Bar Pasek tytułu
# Document naming and account controls.
Rename Document Zmień nazwę dokumentu
Document name Nazwa dokumentu
Rename Zmień nazwę
Account Konto
Privacy Options Opcje prywatności
Trust Center Centrum zaufania
Privacy for this document Prywatność tego dokumentu
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2094,6 +2094,12 @@
Start-up Inicialização
Show the Start screen when WordCraft starts Mostrar a tela inicial ao abrir o WordCraft
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Ativado: o WordCraft abre em Arquivo › Início, com um documento em branco, os modelos e seus documentos recentes. Desativado: abre um documento em branco.
# Document naming and account controls.
Rename Document Renomear documento
Document name Nome do documento
Rename Renomear
Account Conta
Privacy Options Opções de privacidade
Trust Center Central de confiabilidade
Privacy for this document Privacidade deste documento
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2472,6 +2472,12 @@ preview Title Пример
Level 7 Уровень 7
Level 8 Уровень 8
Level 9 Уровень 9
# Document naming and account controls.
Rename Document Переименовать документ
Document name Имя документа
Rename Переименовать
Account Учётная запись
Privacy Options Параметры конфиденциальности
Trust Center Центр управления безопасностью
Privacy for this document Конфиденциальность этого документа
Can't render this file because it contains an unexpected character in line 7 and column 25.
+6
View File
@@ -2092,6 +2092,12 @@
Start-up Pokretanje
Show the Start screen when WordCraft starts Prikaži početni ekran pri pokretanju programa WordCraft
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Uključeno: WordCraft se otvara na Fajl › Početak, sa praznim dokumentom, šablonima i nedavnim dokumentima. Isključeno: otvara prazan dokument.
# Document naming and account controls.
Rename Document Preimenuj dokument
Document name Ime dokumenta
Rename Preimenuj
Account Nalog
Privacy Options Opcije privatnosti
Trust Center Centar za bezbednost
Privacy for this document Privatnost ovog dokumenta
Can't render this file because it contains an unexpected character in line 6 and column 25.
+6
View File
@@ -2096,6 +2096,12 @@
Start-up Покретање
Show the Start screen when WordCraft starts Прикажи почетни екран при покретању програма WordCraft
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Укључено: WordCraft се отвара на Фајл › Почетак, са празним документом, шаблонима и недавним документима. Искључено: отвара празан документ.
# Document naming and account controls.
Rename Document Преименуј документ
Document name Име документа
Rename Преименуј
Account Налог
Privacy Options Опције приватности
Trust Center Центар за безбедност
Privacy for this document Приватност овог документа
Can't render this file because it contains an unexpected character in line 11 and column 25.
+6
View File
@@ -2471,6 +2471,12 @@ preview Title Örnek
Level 7 Düzey 7
Level 8 Düzey 8
Level 9 Düzey 9
# Document naming and account controls.
Rename Document Belgeyi yeniden adlandır
Document name Belge adı
Rename Yeniden adlandır
Account Hesap
Privacy Options Gizlilik Seçenekleri
Trust Center Güven Merkezi
Privacy for this document Bu belgenin gizliliği
Can't render this file because it contains an unexpected character in line 440 and column 13.
+6
View File
@@ -2094,6 +2094,12 @@
Start-up Запуск
Show the Start screen when WordCraft starts Показувати початковий екран під час запуску WordCraft
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Увімкнено: WordCraft відкривається на сторінці Файл › Основне з порожнім документом, шаблонами та нещодавніми документами. Вимкнено: відкривається порожній документ.
# Document naming and account controls.
Rename Document Перейменувати документ
Document name Ім’я документа
Rename Перейменувати
Account Обліковий запис
Privacy Options Параметри конфіденційності
Trust Center Центр безпеки
Privacy for this document Конфіденційність цього документа
Can't render this file because it contains an unexpected character in line 6 and column 31.
+6
View File
@@ -2539,3 +2539,9 @@
Heading text Văn bản đề mục
Insert reference to: Chèn tham chiếu tới:
Reference type: Loại tham chiếu:
# Rename Document (#1016)
Account Tài khoản
Document name Tên tài liệu
Rename Đổi tên
Rename Document Đổi tên tài liệu
Can't render this file because it contains an unexpected character in line 7 and column 25.
+6
View File
@@ -2094,6 +2094,12 @@
Start-up 启动
Show the Start screen when WordCraft starts 启动 WordCraft 时显示开始屏幕
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. 开:WordCraft 打开时显示“文件 › 开始”,其中有空白文档、模板和最近使用的文档。关:直接打开空白文档。
# Document naming and account controls.
Rename Document 重命名文档
Document name 文档名称
Rename 重命名
Account 账户
Privacy Options 隐私选项
Trust Center 信任中心
Privacy for this document 此文档的隐私
Can't render this file because it contains an unexpected character in line 7 and column 25.
+6
View File
@@ -2083,6 +2083,12 @@
Start-up 啟動
Show the Start screen when WordCraft starts 啟動 WordCraft 時顯示開始畫面
On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. 開:WordCraft 開啟時顯示「檔案 › 常用」,其中有空白文件、範本和最近使用的文件。關:直接開啟空白文件。
# Document naming and account controls.
Rename Document 重新命名文件
Document name 文件名稱
Rename 重新命名
Account 帳戶
Privacy Options 隱私權選項
Trust Center 信任中心
Privacy for this document 此文件的隱私
Can't render this file because it contains an unexpected character in line 567 and column 13.
+104 -7
View File
@@ -838,6 +838,7 @@ impl WordApp {
return Ok(json!({"downloaded": name, "bytes": bytes.len()}));
}
let document = self.session.document_id();
let old_path = self.session.path.clone();
let r = self.session.run(id, &params).map_err(|e| e.to_string());
// When the document has been replaced (not when Open only showed its picker), a pending
// "Save changes?" and AutoSave's go-ahead were both about the old one.
@@ -856,6 +857,17 @@ impl WordApp {
{
self.autosave_path = self.session.path.clone();
}
// A renamed file is the same file: AutoSave keeps covering it, and Recent drops the old name.
if r.is_ok()
&& id == "file.rename"
&& let Some(old) = old_path.filter(|old| self.session.path.as_ref() != Some(old))
{
if self.autosave_path.as_ref() == Some(&old) {
self.autosave_path = self.session.path.clone();
}
let old = old.to_string_lossy();
self.ui.recent.retain(|r| *r != old);
}
if let Ok(v) = &r {
self.keep_web_font(id, &params, v);
}
@@ -1557,13 +1569,11 @@ impl WordApp {
pub fn title_stem(&self) -> String {
match &self.session.path {
Some(p) => p.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| tl!("Document1").into()),
None => {
if self.session.doc.core.title.is_empty() {
tl!("Document1").into()
} else {
self.session.doc.core.title.clone()
}
}
None => match &self.session.file_name {
Some(name) => name.clone(),
None if self.session.doc.core.title.is_empty() => tl!("Document1").into(),
None => self.session.doc.core.title.clone(),
},
}
}
@@ -2075,6 +2085,7 @@ fn input_dialog(id: &str, params: &Value) -> Option<&'static str> {
let has = |k: &str| params.get(k).is_some_and(|v| !v.is_null());
let rule = params.get("rule").and_then(Value::as_str).map(str::to_ascii_uppercase);
match id {
"file.rename" if !has("name") => Some("renameDocument"),
"mailings.rules" if !has("field") => match rule.as_deref() {
None | Some("IF") => Some("ruleIf"),
Some("SKIPIF") => Some("ruleSkipIf"),
@@ -2306,6 +2317,63 @@ mod tests {
WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default())
}
#[test]
fn title_and_account_buttons_work() {
use egui_kittest::kittest::Queryable;
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
app(),
);
h.run_steps(6);
h.get_by_label("Document1 — Not saved").click();
h.run_steps(2);
assert_eq!(h.state().dialog.as_ref().map(|d| d.name()), Some("renameDocument"));
h.get_by_role_and_label(egui::accesskit::Role::Button, "Rename").click();
h.run_steps(2);
assert!(h.state().dialog.is_none());
assert!(h.state().session.file_name.is_none());
assert!(!h.state().session.dirty);
h.get_by_label("Document1 — Not saved").click();
h.run_steps(2);
h.event(egui::Event::Key {
key: egui::Key::A,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers { command: true, ..Default::default() },
});
h.event(egui::Event::Text("Project notes".into()));
h.run_steps(2);
h.get_by_role_and_label(egui::accesskit::Role::Button, "Rename").click();
h.run_steps(2);
assert!(h.state().dialog.is_none());
assert_eq!(h.state().title_stem(), "Project notes");
assert_eq!(h.state().default_save_name(), "Project notes.docx");
assert!(h.state().session.doc.core.title.is_empty());
assert!(!h.state().session.dirty);
// A long name remains clickable, and Escape leaves it unchanged.
let long_name = "Notes ".repeat(25);
h.state_mut().run("file.rename", json!({"name": long_name})).unwrap();
h.run_steps(2);
h.get_by_label(&format!("{} — Not saved", long_name.trim())).click();
h.run_steps(2);
assert!(h.state().dialog.is_some());
h.event(egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() });
h.run_steps(2);
assert!(h.state().dialog.is_none());
assert_eq!(h.state().title_stem(), long_name.trim());
h.get_by_label("Account").click();
h.run_steps(2);
assert!(h.state().ui.backstage);
assert_eq!(h.state().ui.backstage_page, "options");
assert_eq!(h.state().ui.options_pane, "general");
assert!(h.query_by_label("User name:").is_some());
}
/// File › Options › Advanced › Editing: alignment guides are a UI command, on by default and
/// kept between runs.
#[test]
@@ -4161,6 +4229,35 @@ mod tests {
}
}
#[test]
fn renaming_a_saved_file_keeps_autosave_and_replaces_its_recent_entry() {
let dir = scratch("rename-autosave");
let path = dir.join("notes.docx");
let renamed = dir.join("Notes v2.docx");
let mut a = typed();
a.run("file.save", json!({"path": path.to_string_lossy()})).unwrap();
assert!(a.autosaves());
a.run("file.rename", json!({"name": "Notes v2"})).unwrap();
assert_eq!(a.session.path.as_deref(), Some(renamed.as_path()));
assert!(a.autosaves(), "AutoSave still covers the renamed file");
assert_eq!(a.ui.recent.first(), Some(&renamed.to_string_lossy().to_string()));
assert!(!a.ui.recent.contains(&path.to_string_lossy().to_string()), "{:?}", a.ui.recent);
a.run("text.insert", json!({"text": "More "})).unwrap();
a.autosave_tick(now_ms() + 10_000.0);
assert!(!a.session.dirty, "AutoSave wrote the renamed file");
let saved = wordcraft_engine::io::open_path(&renamed).unwrap();
assert!(saved.plain_text(wordcraft_doc::StoryRef::Body).contains("More "));
assert!(!path.exists());
// A file opened but not saved in this session doesn't gain AutoSave by being renamed.
let mut b = app();
b.run("file.open", json!({"path": renamed.to_string_lossy()})).unwrap();
assert!(!b.saved_here());
b.run("file.rename", json!({"name": "Notes v3"})).unwrap();
assert!(!b.saved_here() && !b.autosaves());
let _ = std::fs::remove_dir_all(&dir);
}
/// Web: the download callback couldn't report a failure, so Save marked the document saved
/// and the prompt went ahead even when no download started.
#[test]
+4 -1
View File
@@ -1,6 +1,6 @@
# Where WordCraft falls short of Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (ODT tracked insertions and deletions, gap 19; previously trivial: RTF tracked insertions and deletions, gap 19; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (title rename and account settings; previously trivial: ODT tracked insertions and deletions, gap 19; previously trivial: RTF tracked insertions and deletions, gap 19; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
@@ -11,6 +11,8 @@ first (`gh pr list`), and update this file and the parity doc when a gap closes.
Native save reliability: synthetic cases on macOS reproduced loss through the predictable temporary-file name. Saves now use owned, exclusively created siblings, synchronize before replacement and clean up on failure; originals survive pre-replacement errors. This narrows save-path reliability under gap #3; the listed field failures and broader stability work remain open. See [save-path guarantees](file-format-parity.md#native-save-path-guarantees).
Document renaming: desktop renaming requires a filesystem that supports hard links (exFAT/FAT drives and some network shares refuse it). Behaviour: [UI parity](ui-parity.md).
## Ranked
| # | Gap | B | Evidence | Impact | Hours | Doc |
@@ -80,6 +82,7 @@ The same gaps grouped the way the parity documents are, for agents working in on
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | trivial | Document title opens Rename Document; the account badge opens Options › General for user name and initials. |
| 2026-10-13 | trivial | Replace All checks content-control locks before editing; a refused batch leaves the document and undo history unchanged; existing deletion-lock rules reused, with tracked/untracked regression coverage |
| 2026-10-12 | trivial | Sample-text formulas on Enter (#1067) implemented; numerical limits not compared in Word |
| 2026-10-12 | minor | Italian proofing landed (it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar); gap #8 narrowed to the remaining languages |
+2 -2
View File
@@ -45,8 +45,8 @@ config works across the suite) keep the agent to the folders you give it:
`styles.organizer` and `styles.copyFrom` read. The pictures an opened HTML file names are read
from beside that file, so they are inside the read root too (a link that leads out is not followed).
- Every file a command writes must lie inside the **write root**: `save_document`, `file.save`,
`file.saveAs`, `file.exportPdf`, `file.exportPng`, `file.printPdf`, `file.saveTemplate`, and `mailings.finish` with
a `path`.
`file.saveAs`, `file.rename` (the file and its new name), `file.exportPdf`, `file.exportPng`, `file.printPdf`,
`file.saveTemplate`, and `mailings.finish` with a `path`.
- Read and write access are separate. Give the same folder twice for both; a root left out grants
none of its access (with only `--automation-read-root`, nothing can be written to disk). Without
either flag nothing changes.
+4 -1
View File
@@ -1,11 +1,13 @@
# UI parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Cross-reference dialog, #916; previously trivial: web Save Changes prompt waits for the save picker, #1315; previously trivial: AutoFit in a nested table, whole cells shaded for a selection through a table, #971; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Cross-reference dialog, #916; previously trivial: title rename and account settings; previously trivial: web Save Changes prompt waits for the save picker, #1315; previously trivial: AutoFit in a nested table, whole cells shaded for a selection through a table, #971; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
own (`AGENTS.md`: no Microsoft iconography, ever).
Document title opens Rename Document; the account badge opens Options › General for user name and initials. Saved documents retain their format and refuse conflicting names; desktop renaming requires a filesystem that supports hard links. Web renaming changes the next download name. An unsaved document's name is only the name Save suggests: its Title property is left alone.
**Dimension: ~65% (estimated), 59–92 h to full** (the sum of the rows below; dialog depth is counted here, not under features).
| Area | WordCraft | Word | Parity | Hours |
@@ -50,6 +52,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | trivial | Cross-reference dialog: reference type (heading, bookmark, equation, figure, table), what to insert and the item; the ribbon button opened nothing before (#916); modal dialog count 55 |
| 2026-10-12 | trivial | Document title opens Rename Document; the account badge opens Options › General for user name and initials. |
| 2026-10-13 | trivial | The caret is black or white by contrast with the paper, page colour, dark page, shading or highlight under it (#299) |
| 2026-10-13 | trivial | Clicks on ribbon and status-bar chrome that run no command button (Styles and style-set gallery tiles, tab names, empty ribbon space, View check boxes, the status bar's view buttons and language menu) no longer take keyboard focus from the page: the caret stays and typing continues, as in Word; text fields such as the font size box still take focus. Tabs, gallery tiles and the status bar's view buttons report their name and state to screen readers (#1093) |
| 2026-10-13 | trivial | Interface size: a chosen size is absolute ("100%" is one screen pixel per point, even where the system reports 200 % under a bare window manager) and the new default, Auto, keeps the display scale and shows the size it draws at; 75–300 %; an old saved size becomes the size it drew at; on the web a chosen size cancels browser zoom, Auto follows it (#822) |