feat: preserve nested source projects across desktop and REST

This commit is contained in:
2026-09-06 00:17:20 +03:00
parent d364d3c058
commit 4e8575bd11
32 changed files with 4283 additions and 417 deletions
+48 -6
View File
@@ -7,6 +7,7 @@ use iced::{
Element as View, Fill, Font,
widget::{
self, Space, button, column, container, pick_list, row, scrollable, text, text_input,
tooltip,
},
};
use oa_core::*;
@@ -1080,7 +1081,7 @@ fn source_files(app: &App) -> View<'_, Message> {
);
}
}
let mut panel = column![
let panel = column![
text("SOURCE FILES").size(13),
label(format!(
"{} files · one system model",
@@ -1097,9 +1098,6 @@ fn source_files(app: &App) -> View<'_, Message> {
label("Includes define file membership. Closing a detached buffer never deletes its file."),
]
.spacing(9);
if app.pending_recovery.is_some() {
panel = panel.push(action("Recover interrupted save", Message::SourceRecover));
}
panel.into()
}
@@ -1144,8 +1142,49 @@ fn source(app: &App) -> View<'_, Message> {
} else {
column![].into()
};
let recovery: View<'_, Message> = if let Some(path) = &app.pending_recovery {
let edits_block_recovery =
!app.source_workspace.applied && app.source_workspace.has_edits();
let filename = path.file_name().unwrap_or_default().to_string_lossy();
container(
column![
tooltip(
text(format!("Interrupted save · {filename}")).size(15),
text(path.display().to_string()).size(12),
tooltip::Position::Bottom,
),
text(if edits_block_recovery {
"Revert the edited files before recovery. Recovery restores files to their state before the interrupted save, then reopens them."
} else if app.source_workspace.applied {
"Restore files to their state before the interrupted save. Your current model and editor drafts are kept."
} else {
"Restore files to their state before the interrupted save, then reopen them. This rolls back the incomplete save."
})
.size(13),
button(text("Recover interrupted save").size(12))
.padding(10)
.on_press_maybe(
(!edits_block_recovery && !app.busy).then_some(Message::SourceRecover),
),
]
.spacing(8),
)
.padding(12)
.width(Fill)
.style(style::card)
.into()
} else {
column![].into()
};
let mut diagnostics = column![].spacing(5);
let mut visible_diagnostics = 0;
for (index, issue) in app.source_diagnostics.iter().enumerate() {
// The persistent recovery banner supplies the action and explanation;
// repeating the internal journal path obscures ordinary diagnostics.
if app.pending_recovery.is_some() && issue.diagnostic.code == "pending_save" {
continue;
}
visible_diagnostics += 1;
diagnostics = diagnostics.push(
button(
text(format!(
@@ -1216,8 +1255,11 @@ fn source(app: &App) -> View<'_, Message> {
]
.spacing(7)
.align_y(iced::Center),
recovery,
text(app.source_workspace.active.clone()).font(Font::MONOSPACE).size(12).color(style::INK),
label(if !app.source_workspace.applied {
label(if app.pending_recovery.is_some() && !app.source_workspace.applied {
"The previous graphical model remains available while these files are recovered."
} else if !app.source_workspace.applied {
"This project needs repair. Apply all adopts it; the previous graphical model remains available."
} else if app.source_workspace.detached_dirty() {
"A detached file has an unsaved draft. Reattach it or revert that file before saving."
@@ -1226,7 +1268,7 @@ fn source(app: &App) -> View<'_, Message> {
}),
navigation,
scrollable(diagnostics)
.height((app.source_diagnostics.len() as f32 * 32.).min(132.))
.height((visible_diagnostics as f32 * 32.).min(132.))
]
.spacing(12)
)