C14.15 r1: failed recovery and session writes show in the window, once each, with the last kept time
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e1b889cb7f
commit
906d64cf52
6 files changed
+155
-12
No files matched your search
@@ -283,4 +283,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
|
||||
- 2026-10-11:Linux 包的实测(L1.6)。本机(Omarchy,Hyprland 0.56)装上六个 Arch 包与六个 AppImage,`linux-installed.sh` 两种都过;deb、rpm、Arch 包在 Ubuntu 26.04、Fedora 44、Arch 容器里装上、全部 ELF 找得到库;deb 与 rpm 的六个应用在各自容器的 Xvfb 下画出窗口(`linux-distros.sh`)。截图按窗口(`grim -T` 加 Hyprland 的 `stableId`):按区域截时用户切换工作区或别的窗口盖住即截错。ElyMark 的文件监视略去读取后不会自激:重读前先比指纹(长度与修改时间),不打开文件。
|
||||
- 2026-10-11(用户定:替换 v0.1.3 的 Linux 包):v0.1.3 发布里原有的六个 AppImage(无头,打不开窗口)已删去,换成本机容器里建出的六个应用各四种包(共 24 个),出自 L1.7 的提交(晚于该标签);上传后逐个核对大小,ElyTree 的 AppImage 下载回来散列相同。`package.sh` 把每个 RUNPATH 按所在 ELF 解析(`$ORIGIN` 或 `${ORIGIN}` 开头,规范化后须在 AppDir 内),否则拒绝(如未修正的 cairo):CI 用的是别处建好的引擎,旧构建会在这里报错而不是带进包里。替换的 Linux 包对应的源码随之上传到同一发布(`source.sh`,文件名带构建提交号),发布说明写明 Linux 包出自哪个提交。
|
||||
- 2026-10-11:`ci.sh` 用 `ELY_SHARE` 时,CI 克隆里的 `engines`、`reference` 须是指向共享克隆的链接(Windows 为目录联接,不查);是本克隆自建的目录或指向别处即报错,不混用。`.gitignore` 的 `/engines`、`/reference` 不带斜杠,链接也被忽略(`source.sh` 要求工作区干净)。
|
||||
- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。组件库 f59c9f7:文字超过读取上限(2 MiB)即丢掉旧的读取,大纲与块为空。ElyMark(C14.4):视图即一个 `MarkdownDocument`,文本即文件原文(BOM 与 CRLF 照 `text.rs` 存回),新建、打开、保存、另存、重命名经外壳照旧;视图在建成后的下一轮更新向外壳报一次载入(文档在 `new` 里当场解析,外壳此时还未订阅);文字超过 2 MiB 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印Line truncated
|
||||
- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。组件库 f59c9f7:文字超过读取上限(2 MiB)即丢掉旧的读取,大纲与块为空。ElyMark(C14.4):视图即一个 `MarkdownDocument`,文本即文件原文(BOM 与 CRLF 照 `text.rs` 存回),新建、打开、保存、另存、重命名经外壳照旧;视图在建成后的下一轮更新向外壳报一次载入(文档在 `new` 里当场解析,外壳此时还未订阅);文字超过 2 MiB 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印Line truncated
|
||||
@@ -51,6 +51,9 @@ pub struct Extra {
|
||||
/// The last recovery point's mark, and one in flight.
|
||||
pub pointed: Option<String>,
|
||||
pub pointing: bool,
|
||||
/// When a point last landed, and the failure last shown.
|
||||
pub pointed_at: Option<std::time::SystemTime>,
|
||||
pub point_failed: Option<String>,
|
||||
/// A kept tab's mode, place and text came back once.
|
||||
pub restored: bool,
|
||||
/// Disk reads started; only the newest lands.
|
||||
@@ -79,6 +82,8 @@ impl Default for Extra {
|
||||
slot: String::new(),
|
||||
pointed: None,
|
||||
pointing: false,
|
||||
pointed_at: None,
|
||||
point_failed: None,
|
||||
restored: false,
|
||||
reads: 0,
|
||||
looked: false,
|
||||
|
||||
@@ -46,6 +46,8 @@ pub struct Keeping {
|
||||
crash: Option<Crash>,
|
||||
waiting: Vec<Waiting>,
|
||||
written: Option<String>,
|
||||
/// The session write failure last shown.
|
||||
unwritten: Option<String>,
|
||||
/// A quit began: the kept list stays as then.
|
||||
pub leaving: bool,
|
||||
/// The list as kept when the quit began.
|
||||
@@ -129,6 +131,7 @@ impl Keeping {
|
||||
crash: Some(crash),
|
||||
waiting,
|
||||
written: None,
|
||||
unwritten: None,
|
||||
leaving: false,
|
||||
frozen: Vec::new(),
|
||||
};
|
||||
@@ -284,7 +287,7 @@ impl Desk {
|
||||
recovery.write_once(current.as_bytes(), None, now)
|
||||
})
|
||||
.await;
|
||||
desk.update(cx, |desk, _| desk.pointed(id, point, wrote, &slot))
|
||||
desk.update(cx, |desk, cx| desk.pointed(id, point, wrote, &slot, cx))
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
@@ -304,11 +307,18 @@ impl Desk {
|
||||
}
|
||||
if !self.keeping.leaving {
|
||||
self.keeping.frozen = kept.clone();
|
||||
self.write_session(Saved { tabs: kept });
|
||||
self.write_session(Saved { tabs: kept }, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn pointed(&mut self, id: u64, point: String, wrote: io::Result<Option<PathBuf>>, slot: &Path) {
|
||||
fn pointed(
|
||||
&mut self,
|
||||
id: u64,
|
||||
point: String,
|
||||
wrote: io::Result<Option<PathBuf>>,
|
||||
slot: &Path,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// The tab closed while it wrote: the point goes too.
|
||||
let Some(tab) = self.tab_mut(id) else {
|
||||
if let Err(error) = clear(slot) {
|
||||
@@ -317,21 +327,50 @@ impl Desk {
|
||||
return;
|
||||
};
|
||||
tab.extra.pointing = false;
|
||||
match wrote {
|
||||
Ok(_) => tab.extra.pointed = Some(point),
|
||||
Err(error) => log::error!("mark: a recovery point for tab {id} failed: {error}"),
|
||||
let error = match wrote {
|
||||
Ok(_) => {
|
||||
tab.extra.pointed = Some(point);
|
||||
tab.extra.pointed_at = Some(SystemTime::now());
|
||||
tab.extra.point_failed = None;
|
||||
return;
|
||||
}
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
log::error!("mark: a recovery point for tab {id} failed: {error}");
|
||||
// Each failure shows once; the next tick retries.
|
||||
if tab.extra.point_failed.as_ref() == Some(&error) {
|
||||
return;
|
||||
}
|
||||
tab.extra.point_failed = Some(error.clone());
|
||||
let at = match tab.extra.pointed_at {
|
||||
Some(at) => humantime(at),
|
||||
None => text(cx, "office.stopped.never", &[]).to_string(),
|
||||
};
|
||||
let name = tab.name.to_string();
|
||||
let said = text(cx, "mark.recovery.failed", &[("name", &name), ("error", &error), ("at", &at)]);
|
||||
self.toast_unlogged(said, ely_gpui_component::primitives::Severity::Danger, cx);
|
||||
}
|
||||
|
||||
fn write_session(&mut self, saved: Saved) {
|
||||
fn write_session(&mut self, saved: Saved, cx: &mut Context<Self>) {
|
||||
let json = serde_json::to_string_pretty(&saved).expect("the session serializes");
|
||||
if self.keeping.written.as_ref() == Some(&json) {
|
||||
return;
|
||||
}
|
||||
let file = self.keeping.dir.join("session.json");
|
||||
match ely_files::save_atomic(&file, json.as_bytes(), 0) {
|
||||
Ok(_) => self.keeping.written = Some(json),
|
||||
Err(error) => log::error!("mark: the session did not save: {error}"),
|
||||
let error = match ely_files::save_atomic(&file, json.as_bytes(), 0) {
|
||||
Ok(_) => {
|
||||
self.keeping.written = Some(json);
|
||||
self.keeping.unwritten = None;
|
||||
return;
|
||||
}
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
log::error!("mark: the session did not save: {error}");
|
||||
// Each failure shows once; the next tick retries.
|
||||
if self.keeping.unwritten.as_ref() != Some(&error) {
|
||||
self.keeping.unwritten = Some(error.clone());
|
||||
let said = text(cx, "mark.session.failed", &[("error", &error)]);
|
||||
self.toast(said, ely_gpui_component::primitives::Severity::Danger, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +418,7 @@ impl Desk {
|
||||
}
|
||||
}
|
||||
let tabs = self.keeping.frozen.clone();
|
||||
self.write_session(Saved { tabs });
|
||||
self.write_session(Saved { tabs }, cx);
|
||||
}
|
||||
|
||||
/// A quit or close begins: the tabs now stay kept.
|
||||
|
||||
@@ -219,4 +219,94 @@ mod window {
|
||||
);
|
||||
assert!(read.is_err(), "an unknown mode is no default");
|
||||
}
|
||||
|
||||
/// The window's toasts that hold `words`.
|
||||
fn toasted(desk: &Entity<Desk>, words: &str, cx: &mut VisualTestContext) -> usize {
|
||||
let root = desk.read_with(cx, |desk, _| desk.core.root.upgrade().expect("the window"));
|
||||
let toasts = root.read_with(cx, |root, cx| root.toasts(cx));
|
||||
toasts
|
||||
.iter()
|
||||
.filter(|(said, _)| said.contains(words))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_failed_recovery_write_shows_once_with_the_last_kept(cx: &mut TestAppContext) {
|
||||
let (data, files) = (
|
||||
tempfile::tempdir().expect("data"),
|
||||
tempfile::tempdir().expect("files"),
|
||||
);
|
||||
let a = files.path().join("a.md");
|
||||
std::fs::write(
|
||||
&a, "Text.
|
||||
",
|
||||
)
|
||||
.expect("a");
|
||||
let (desk, cx) = start(cx, data.path());
|
||||
open(&desk, &a, cx);
|
||||
let view = show(&desk, "a.md", cx);
|
||||
for (typed, broken) in [
|
||||
(
|
||||
"One.
|
||||
", false,
|
||||
),
|
||||
(
|
||||
"Two.
|
||||
", true,
|
||||
),
|
||||
(
|
||||
"Three.
|
||||
", true,
|
||||
),
|
||||
] {
|
||||
if broken && data.path().join("recovery").is_dir() {
|
||||
std::fs::remove_dir_all(data.path().join("recovery")).expect("removes");
|
||||
std::fs::write(data.path().join("recovery"), "not a folder").expect("blocks");
|
||||
}
|
||||
view.update(cx, |view, cx| view.adopt(typed.into(), cx));
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
cx.run_until_parked();
|
||||
desk.update(cx, |desk, cx| desk.keep(cx));
|
||||
cx.run_until_parked();
|
||||
}
|
||||
assert_eq!(toasted(&desk, "was not kept for recovery", cx), 1, "once");
|
||||
let never = toasted(&desk, "none yet", cx);
|
||||
assert_eq!(never, 0, "the first point landed, so its time shows");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_failed_session_write_shows_once(cx: &mut TestAppContext) {
|
||||
let (data, files) = (
|
||||
tempfile::tempdir().expect("data"),
|
||||
tempfile::tempdir().expect("files"),
|
||||
);
|
||||
let a = files.path().join("a.md");
|
||||
std::fs::write(
|
||||
&a, "Text.
|
||||
",
|
||||
)
|
||||
.expect("a");
|
||||
let (desk, cx) = start(cx, data.path());
|
||||
open(&desk, &a, cx);
|
||||
let session = data.path().join("session.json");
|
||||
if session.exists() {
|
||||
std::fs::remove_file(&session).expect("removes");
|
||||
}
|
||||
std::fs::create_dir(&session).expect("blocks");
|
||||
for line in [1, 2] {
|
||||
let view = show(&desk, "a.md", cx);
|
||||
view.update(cx, |view, cx| {
|
||||
view.document()
|
||||
.update(cx, |document, cx| document.set_caret(line, 0, cx))
|
||||
});
|
||||
desk.update(cx, |desk, cx| desk.keep(cx));
|
||||
cx.run_until_parked();
|
||||
}
|
||||
assert_eq!(
|
||||
toasted(&desk, "were not saved for the next launch", cx),
|
||||
1,
|
||||
"once"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,8 @@ pub(super) const EN: &[(&str, &str)] = &[
|
||||
("mark.export.formula", "line {line}, {error}"),
|
||||
("mark.export.folder_taken", "{name} is already there; the pictures need that folder."),
|
||||
("mark.copy.rich", "Copy as Rich Text"),
|
||||
("mark.recovery.failed", "Unsaved text of {name} was not kept for recovery: {error}. Last kept: {at}."),
|
||||
("mark.session.failed", "The open tabs were not saved for the next launch: {error}"),
|
||||
("mark.copy.failed", "Copying failed: {error}"),
|
||||
("mark.export.failed", "The export failed: {error}"),
|
||||
("mark.look.focus", "Focus Mode"),
|
||||
@@ -157,6 +159,8 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
|
||||
("mark.export.formula", "第 {line} 行,{error}"),
|
||||
("mark.export.folder_taken", "{name} 已存在;图片要放进这个文件夹。"),
|
||||
("mark.copy.rich", "复制为富文本"),
|
||||
("mark.recovery.failed", "{name} 未保存的文字没能存为恢复点:{error}。上次存下:{at}。"),
|
||||
("mark.session.failed", "打开的标签没能存下,下次启动可能不会恢复:{error}"),
|
||||
("mark.copy.failed", "复制失败:{error}"),
|
||||
("mark.export.failed", "导出失败:{error}"),
|
||||
("mark.look.focus", "专注模式"),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
FAIL
|
||||
|
||||
1. apps/ely-mark/src/restore.rs:322 — Recovery-write failures only reach the log. For an unsaved tab, make the recovery destination unwritable (the probe replaces `recovery/` with a regular file), then run `keep`: the write finishes unsuccessfully, `pointed` remains absent, and the window shows no error or last successful recovery time. A subsequent crash loses this text without warning. This violates PRD §6.2 and UX-06. Pass the failure back to the window, retain and expose the last successful recovery time, and add a window regression for failed recovery writes. WSL probe `a_failed_recovery_write_is_visible` fails with `TOASTS before=[], after=[]`; evidence: `.work/C14.15-r1/probe-test.log`.
|
||||
|
||||
2. apps/ely-mark/src/restore.rs:334 — Session-write failures also only reach the log. If `session.json` cannot be replaced (the probe creates a directory at that path), `keep` returns without telling the user that the tabs, modes, caret and scroll were not persisted. A crash then leaves the previous session snapshot, or none, so the latest session cannot restore. This violates MARK-12 and UX-06. Surface the failed session write in the window while retaining the previous snapshot and retry behavior; add a window regression for this failure. WSL probe `a_failed_session_write_is_visible` fails with `TOASTS before=[], after=[]`; evidence: `.work/C14.15-r1/probe-test.log`. Baseline `cargo test -p ely-mark` passes on Windows (88 tests) and WSL Linux (89 tests); logs are in the same folder. Probe source copies, their target directory and both platforms' incremental caches were deleted before this verdict.
|
||||
Reference in new issue
Block a user