Restore project workspace state
This commit is contained in:
@@ -6,7 +6,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
||||
|
||||
## Available Features
|
||||
|
||||
- Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars, relative-dated entity lists with row deletion, dialogs, tasks, embedded Wry previews, and a live Pico CSS theme editor.
|
||||
- Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars, relative-dated entity lists with row deletion, dialogs, tasks, embedded Wry previews, a live Pico CSS theme editor, and per-project restart restoration of the active activity, shell visibility, and open editor tabs.
|
||||
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, live link/backlink graphs, media, and batch gallery-image import.
|
||||
- Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
|
||||
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
||||
|
||||
@@ -33,6 +33,7 @@ pub mod tag;
|
||||
pub mod task;
|
||||
pub mod template;
|
||||
pub mod template_rebuild;
|
||||
pub mod ui_state;
|
||||
pub mod validate_media;
|
||||
pub mod validate_site;
|
||||
pub mod validate_translations;
|
||||
|
||||
146
crates/bds-core/src/engine/ui_state.rs
Normal file
146
crates/bds-core/src/engine/ui_state.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::DbConnection as Connection;
|
||||
use crate::db::queries::{project, setting};
|
||||
use crate::engine::{EngineError, EngineResult};
|
||||
use crate::util::now_unix_ms;
|
||||
|
||||
const KEY_SUFFIX: &str = "ui_state";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ProjectUiState {
|
||||
pub sidebar_view: String,
|
||||
pub sidebar_visible: bool,
|
||||
pub sidebar_width: f32,
|
||||
pub panel_visible: bool,
|
||||
pub panel_tab: String,
|
||||
pub tabs: Vec<PersistedTab>,
|
||||
pub active_tab: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ProjectUiState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sidebar_view: "posts".to_string(),
|
||||
sidebar_visible: true,
|
||||
sidebar_width: 280.0,
|
||||
panel_visible: false,
|
||||
panel_tab: "tasks".to_string(),
|
||||
tabs: Vec::new(),
|
||||
active_tab: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PersistedTab {
|
||||
pub tab_type: String,
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub is_transient: bool,
|
||||
}
|
||||
|
||||
pub fn load(conn: &Connection, project_id: &str) -> EngineResult<Option<ProjectUiState>> {
|
||||
match setting::get_setting_by_key(conn, &key(project_id)) {
|
||||
Ok(setting) => Ok(Some(serde_json::from_str(&setting.value)?)),
|
||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
Err(error) => Err(EngineError::Db(error)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(conn: &Connection, project_id: &str, state: &ProjectUiState) -> EngineResult<()> {
|
||||
match project::get_project_by_id(conn, project_id) {
|
||||
Ok(_) => {}
|
||||
Err(diesel::result::Error::NotFound) => {
|
||||
return Err(EngineError::NotFound(format!("project {project_id}")));
|
||||
}
|
||||
Err(error) => return Err(EngineError::Db(error)),
|
||||
}
|
||||
let serialized = serde_json::to_string(state)?;
|
||||
setting::set_setting_value(conn, &key(project_id), &serialized, now_unix_ms())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn key(project_id: &str) -> String {
|
||||
format!("project:{project_id}:{KEY_SUFFIX}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::Database;
|
||||
use crate::db::queries::project::{insert_project, make_test_project};
|
||||
|
||||
fn setup() -> Database {
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p1", "one")).unwrap();
|
||||
insert_project(db.conn(), &make_test_project("p2", "two")).unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_independent_project_sessions() {
|
||||
let db = setup();
|
||||
let one = ProjectUiState {
|
||||
sidebar_view: "media".into(),
|
||||
sidebar_visible: false,
|
||||
sidebar_width: 412.0,
|
||||
panel_visible: true,
|
||||
panel_tab: "output".into(),
|
||||
tabs: vec![PersistedTab {
|
||||
tab_type: "post".into(),
|
||||
id: "post-1".into(),
|
||||
title: "Post one".into(),
|
||||
is_transient: false,
|
||||
}],
|
||||
active_tab: Some("post-1".into()),
|
||||
};
|
||||
let two = ProjectUiState {
|
||||
sidebar_view: "scripts".into(),
|
||||
tabs: vec![PersistedTab {
|
||||
tab_type: "scripts".into(),
|
||||
id: "script-2".into(),
|
||||
title: "Script two".into(),
|
||||
is_transient: true,
|
||||
}],
|
||||
active_tab: Some("script-2".into()),
|
||||
..ProjectUiState::default()
|
||||
};
|
||||
|
||||
save(db.conn(), "p1", &one).unwrap();
|
||||
save(db.conn(), "p2", &two).unwrap();
|
||||
|
||||
assert_eq!(load(db.conn(), "p1").unwrap(), Some(one));
|
||||
assert_eq!(load(db.conn(), "p2").unwrap(), Some(two));
|
||||
assert_eq!(load(db.conn(), "missing").unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_newer_fields_use_safe_defaults() {
|
||||
let db = setup();
|
||||
setting::set_setting_value(db.conn(), &key("p1"), r#"{"sidebar_view":"media"}"#, 1)
|
||||
.unwrap();
|
||||
|
||||
let state = load(db.conn(), "p1").unwrap().unwrap();
|
||||
assert_eq!(state.sidebar_view, "media");
|
||||
assert!(state.sidebar_visible);
|
||||
assert_eq!(state.sidebar_width, 280.0);
|
||||
assert!(state.tabs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_a_project_removes_its_session() {
|
||||
let db = setup();
|
||||
save(db.conn(), "p1", &ProjectUiState::default()).unwrap();
|
||||
|
||||
crate::engine::project::delete_project(db.conn(), "p1", None).unwrap();
|
||||
|
||||
assert_eq!(load(db.conn(), "p1").unwrap(), None);
|
||||
assert!(matches!(
|
||||
save(db.conn(), "p1", &ProjectUiState::default()),
|
||||
Err(EngineError::NotFound(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@ impl BdsApp {
|
||||
if operation == GitOperation::Commit {
|
||||
self.git_state.commit_message.clear();
|
||||
self.close_git_diff_tabs();
|
||||
self.persist_project_ui_state();
|
||||
}
|
||||
self.notify(
|
||||
ToastLevel::Success,
|
||||
@@ -340,7 +341,7 @@ impl BdsApp {
|
||||
)
|
||||
}
|
||||
|
||||
fn open_git_file_diff(&mut self, path: String) -> Task<Message> {
|
||||
pub(super) fn open_git_file_diff(&mut self, path: String) -> Task<Message> {
|
||||
let tab = crate::views::git::file_tab(&path);
|
||||
let tab_id = tab.id.clone();
|
||||
self.activate_git_tab(tab);
|
||||
@@ -373,7 +374,7 @@ impl BdsApp {
|
||||
)
|
||||
}
|
||||
|
||||
fn open_git_commit_diff(&mut self, hash: String, subject: String) -> Task<Message> {
|
||||
pub(super) fn open_git_commit_diff(&mut self, hash: String, subject: String) -> Task<Message> {
|
||||
let tab = crate::views::git::commit_tab(&hash, &subject);
|
||||
let tab_id = tab.id.clone();
|
||||
self.activate_git_tab(tab);
|
||||
@@ -462,6 +463,7 @@ impl BdsApp {
|
||||
self.active_tab = self.tabs.get(index).map(|tab| tab.id.clone());
|
||||
self.enforce_panel_tab_fallback();
|
||||
self.sync_menu_state();
|
||||
self.persist_project_ui_state();
|
||||
}
|
||||
|
||||
fn close_git_diff_tabs(&mut self) {
|
||||
@@ -559,6 +561,7 @@ impl BdsApp {
|
||||
}
|
||||
self.enforce_panel_tab_fallback();
|
||||
self.sync_menu_state();
|
||||
self.persist_project_ui_state();
|
||||
}
|
||||
|
||||
pub(super) fn reset_git_for_project_change(&mut self) {
|
||||
|
||||
@@ -23,6 +23,37 @@ impl fmt::Display for SidebarView {
|
||||
}
|
||||
|
||||
impl SidebarView {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Posts => "posts",
|
||||
Self::Pages => "pages",
|
||||
Self::Media => "media",
|
||||
Self::Scripts => "scripts",
|
||||
Self::Templates => "templates",
|
||||
Self::Tags => "tags",
|
||||
Self::Chat => "chat",
|
||||
Self::Import => "import",
|
||||
Self::Git => "git",
|
||||
Self::Settings => "settings",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_persisted(value: &str) -> Option<Self> {
|
||||
Some(match value {
|
||||
"posts" => Self::Posts,
|
||||
"pages" => Self::Pages,
|
||||
"media" => Self::Media,
|
||||
"scripts" => Self::Scripts,
|
||||
"templates" => Self::Templates,
|
||||
"tags" => Self::Tags,
|
||||
"chat" => Self::Chat,
|
||||
"import" => Self::Import,
|
||||
"git" => Self::Git,
|
||||
"settings" => Self::Settings,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the `activity.*` i18n key for this sidebar view.
|
||||
pub fn i18n_key(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -51,6 +82,27 @@ pub enum PanelTab {
|
||||
GitLog,
|
||||
}
|
||||
|
||||
impl PanelTab {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Tasks => "tasks",
|
||||
Self::Output => "output",
|
||||
Self::PostLinks => "post_links",
|
||||
Self::GitLog => "git_log",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_persisted(value: &str) -> Option<Self> {
|
||||
Some(match value {
|
||||
"tasks" => Self::Tasks,
|
||||
"output" => Self::Output,
|
||||
"post_links" => Self::PostLinks,
|
||||
"git_log" => Self::GitLog,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot of a running or completed task shown in the panel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaskSnapshot {
|
||||
@@ -127,4 +179,32 @@ mod tests {
|
||||
assert_eq!(SidebarView::Posts.to_string(), "activity.posts");
|
||||
assert_eq!(SidebarView::Settings.to_string(), "common.settings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_navigation_names_round_trip() {
|
||||
for view in [
|
||||
SidebarView::Posts,
|
||||
SidebarView::Pages,
|
||||
SidebarView::Media,
|
||||
SidebarView::Scripts,
|
||||
SidebarView::Templates,
|
||||
SidebarView::Tags,
|
||||
SidebarView::Chat,
|
||||
SidebarView::Import,
|
||||
SidebarView::Git,
|
||||
SidebarView::Settings,
|
||||
] {
|
||||
assert_eq!(SidebarView::from_persisted(view.as_str()), Some(view));
|
||||
}
|
||||
for tab in [
|
||||
PanelTab::Tasks,
|
||||
PanelTab::Output,
|
||||
PanelTab::PostLinks,
|
||||
PanelTab::GitLog,
|
||||
] {
|
||||
assert_eq!(PanelTab::from_persisted(tab.as_str()), Some(tab));
|
||||
}
|
||||
assert_eq!(SidebarView::from_persisted("unknown"), None);
|
||||
assert_eq!(PanelTab::from_persisted("unknown"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,55 @@ pub enum TabType {
|
||||
}
|
||||
|
||||
impl TabType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Post => "post",
|
||||
Self::Media => "media",
|
||||
Self::Settings => "settings",
|
||||
Self::Style => "style",
|
||||
Self::Tags => "tags",
|
||||
Self::Chat => "chat",
|
||||
Self::Import => "import",
|
||||
Self::MenuEditor => "menu_editor",
|
||||
Self::MetadataDiff => "metadata_diff",
|
||||
Self::GitDiff => "git_diff",
|
||||
Self::Scripts => "scripts",
|
||||
Self::Templates => "templates",
|
||||
Self::Documentation => "documentation",
|
||||
Self::ApiDocumentation => "api_documentation",
|
||||
Self::CliDocumentation => "cli_documentation",
|
||||
Self::McpDocumentation => "mcp_documentation",
|
||||
Self::SiteValidation => "site_validation",
|
||||
Self::TranslationValidation => "translation_validation",
|
||||
Self::FindDuplicates => "find_duplicates",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_persisted(value: &str) -> Option<Self> {
|
||||
Some(match value {
|
||||
"post" => Self::Post,
|
||||
"media" => Self::Media,
|
||||
"settings" => Self::Settings,
|
||||
"style" => Self::Style,
|
||||
"tags" => Self::Tags,
|
||||
"chat" => Self::Chat,
|
||||
"import" => Self::Import,
|
||||
"menu_editor" => Self::MenuEditor,
|
||||
"metadata_diff" => Self::MetadataDiff,
|
||||
"git_diff" => Self::GitDiff,
|
||||
"scripts" => Self::Scripts,
|
||||
"templates" => Self::Templates,
|
||||
"documentation" => Self::Documentation,
|
||||
"api_documentation" => Self::ApiDocumentation,
|
||||
"cli_documentation" => Self::CliDocumentation,
|
||||
"mcp_documentation" => Self::McpDocumentation,
|
||||
"site_validation" => Self::SiteValidation,
|
||||
"translation_validation" => Self::TranslationValidation,
|
||||
"find_duplicates" => Self::FindDuplicates,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Singleton tool tabs per tabs.allium: always one instance, id = type name.
|
||||
///
|
||||
/// Singleton types (12): settings, tags, style, menu_editor,
|
||||
@@ -288,4 +337,32 @@ mod tests {
|
||||
open_tab(&mut tabs, make_tab("c2", TabType::Chat, false));
|
||||
assert_eq!(tabs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_tab_type_names_round_trip() {
|
||||
for tab_type in [
|
||||
TabType::Post,
|
||||
TabType::Media,
|
||||
TabType::Settings,
|
||||
TabType::Style,
|
||||
TabType::Tags,
|
||||
TabType::Chat,
|
||||
TabType::Import,
|
||||
TabType::MenuEditor,
|
||||
TabType::MetadataDiff,
|
||||
TabType::GitDiff,
|
||||
TabType::Scripts,
|
||||
TabType::Templates,
|
||||
TabType::Documentation,
|
||||
TabType::ApiDocumentation,
|
||||
TabType::CliDocumentation,
|
||||
TabType::McpDocumentation,
|
||||
TabType::SiteValidation,
|
||||
TabType::TranslationValidation,
|
||||
TabType::FindDuplicates,
|
||||
] {
|
||||
assert_eq!(TabType::from_persisted(tab_type.as_str()), Some(tab_type));
|
||||
}
|
||||
assert_eq!(TabType::from_persisted("unknown"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ activity-aiAssistant = KI-Assistent
|
||||
activity-import = Importieren
|
||||
activity-sourceControl = Versionskontrolle
|
||||
common-save = Speichern
|
||||
common-workspaceState = Arbeitsbereichszustand
|
||||
common-open = Öffnen
|
||||
common-cancel = Abbrechen
|
||||
common-delete = Löschen
|
||||
|
||||
@@ -89,6 +89,7 @@ activity-aiAssistant = AI Assistant
|
||||
activity-import = Import
|
||||
activity-sourceControl = Source Control
|
||||
common-save = Save
|
||||
common-workspaceState = Workspace state
|
||||
common-open = Open
|
||||
common-cancel = Cancel
|
||||
common-delete = Delete
|
||||
|
||||
@@ -89,6 +89,7 @@ activity-aiAssistant = Asistente IA
|
||||
activity-import = Importar
|
||||
activity-sourceControl = Control de código fuente
|
||||
common-save = Guardar
|
||||
common-workspaceState = Estado del espacio de trabajo
|
||||
common-open = Abrir
|
||||
common-cancel = Cancelar
|
||||
common-delete = Eliminar
|
||||
|
||||
@@ -89,6 +89,7 @@ activity-aiAssistant = Assistant IA
|
||||
activity-import = Importation
|
||||
activity-sourceControl = Contrôle de source
|
||||
common-save = Enregistrer
|
||||
common-workspaceState = État de l’espace de travail
|
||||
common-open = Ouvrir
|
||||
common-cancel = Annuler
|
||||
common-delete = Supprimer
|
||||
|
||||
@@ -89,6 +89,7 @@ activity-aiAssistant = Assistente IA
|
||||
activity-import = Importa
|
||||
activity-sourceControl = Controllo sorgente
|
||||
common-save = Salva
|
||||
common-workspaceState = Stato dell’area di lavoro
|
||||
common-open = Apri
|
||||
common-cancel = Annulla
|
||||
common-delete = Elimina
|
||||
|
||||
@@ -96,6 +96,9 @@ value ShellVisibility {
|
||||
sidebar_visible: Boolean
|
||||
panel_visible: Boolean
|
||||
assistant_sidebar_visible: Boolean
|
||||
-- Existing shell fields are stored per project with the tab session;
|
||||
-- see tabs.allium ProjectUiSession. RuDS currently has no assistant sidebar,
|
||||
-- so only the sidebar and bottom-panel fields participate there.
|
||||
}
|
||||
|
||||
surface ShellVisibilitySurface {
|
||||
|
||||
@@ -49,7 +49,7 @@ entity Project {
|
||||
-- the SQLite database, the per-project embeddings index
|
||||
-- (projects/{id}/embeddings.usearch + .meta.json sidecar), the
|
||||
-- downloaded embedding-model cache, the project registry, and
|
||||
-- window/UI state.
|
||||
-- window/UI state (project:{id}:ui_state in the SQLite settings table).
|
||||
-- OS-specific base (resolved via :filename.basedir, app name "bds"):
|
||||
-- macOS: ~/Library/Application Support/bds (:user_config)
|
||||
-- Linux: $XDG_CONFIG_HOME/bds (default ~/.config/bds)
|
||||
|
||||
@@ -206,7 +206,9 @@ entity PostMediaLink {
|
||||
|
||||
entity Setting {
|
||||
key: String -- Primary key
|
||||
-- Per-project UI sessions use project:{id}:ui_state.
|
||||
value: String -- Serialized value
|
||||
-- UI session values are version-tolerant JSON.
|
||||
updated_at: Timestamp
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ surface TabRuntimeSurface {
|
||||
provides:
|
||||
TabOpening(tab_type, intent)
|
||||
ActiveTabChanged(active_tab)
|
||||
ProjectUiStateChanged(project_id)
|
||||
ProjectActivated(project_id)
|
||||
ApplicationClosing(project_id)
|
||||
}
|
||||
|
||||
-- ─── Tab types ────────────────────────────────────────────────
|
||||
@@ -247,5 +250,74 @@ invariant DirtyIndicator {
|
||||
|
||||
-- ─── Tab state persistence ───────────────────────────────────
|
||||
|
||||
-- Tab state (list + activeTabId) can be serialized/restored
|
||||
-- for session continuity across project switches.
|
||||
value ProjectUiSession {
|
||||
project_id: String
|
||||
active_view: String
|
||||
sidebar_visible: Boolean
|
||||
sidebar_width: Integer
|
||||
panel_visible: Boolean
|
||||
panel_tab: String
|
||||
tabs: List<Tab>
|
||||
active_tab_id: String?
|
||||
}
|
||||
|
||||
surface ProjectUiSessionSurface {
|
||||
context session: ProjectUiSession
|
||||
|
||||
exposes:
|
||||
session.project_id
|
||||
session.active_view
|
||||
session.sidebar_visible
|
||||
session.sidebar_width
|
||||
session.panel_visible
|
||||
session.panel_tab
|
||||
session.tabs
|
||||
session.active_tab_id when session.active_tab_id != null
|
||||
}
|
||||
|
||||
rule PersistProjectUiSession {
|
||||
when: ProjectUiStateChanged(project_id)
|
||||
let session = current_project_ui_session(project_id)
|
||||
ensures: DatabaseSettingWritten(
|
||||
key: format("project:{project_id}:ui_state", project_id: project_id),
|
||||
value: serialize_json(session)
|
||||
)
|
||||
-- Persist after every shell/tab mutation, so the database already
|
||||
-- represents the last visible state if the process is closed.
|
||||
}
|
||||
|
||||
rule PersistProjectUiSessionOnClose {
|
||||
when: ApplicationClosing(project_id)
|
||||
let session = current_project_ui_session(project_id)
|
||||
ensures: DatabaseProjectUiSession(project_id) = session
|
||||
}
|
||||
|
||||
rule RestoreProjectUiSession {
|
||||
when: ProjectActivated(project_id)
|
||||
let session = DatabaseProjectUiSession(project_id)
|
||||
if session != null:
|
||||
ensures: active_view = valid_sidebar_view_or_default(session.active_view, posts)
|
||||
ensures: sidebar_visible = session.sidebar_visible
|
||||
ensures: sidebar_width = clamp(session.sidebar_width, 200, 500)
|
||||
ensures: panel_visible = session.panel_visible
|
||||
ensures: panel_tab = valid_panel_tab_or_default(session.panel_tab, tasks)
|
||||
ensures: tabs = existing_project_tabs(session.tabs, project_id)
|
||||
-- Stale, unknown, and cross-project entity references are discarded.
|
||||
ensures: active_tab = valid_restored_active_tab_or_null(session.active_tab_id, tabs)
|
||||
ensures: EditorsHydratedFromCurrentRecords(tabs, project_id)
|
||||
-- Titles and editor content are resolved afresh; unsaved editor bodies
|
||||
-- are never serialized as UI session state.
|
||||
else:
|
||||
ensures: active_view = posts
|
||||
ensures: sidebar_visible = true
|
||||
ensures: sidebar_width = 280
|
||||
ensures: panel_visible = false
|
||||
ensures: panel_tab = tasks
|
||||
ensures: tabs = empty
|
||||
ensures: active_tab = null
|
||||
}
|
||||
|
||||
invariant ProjectUiSessionIsolation {
|
||||
-- Each project has an independent database session. Activating one project
|
||||
-- never retains tabs, editor state, or shell state from another project.
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ entity TuiState {
|
||||
config {
|
||||
-- Same preference sections as the settings backends; matches the
|
||||
-- GUI settings nav minus the GUI-only Style tab.
|
||||
tui_settings_sections: List<String> = [
|
||||
tui_settings_sections: Set<String> = {
|
||||
"project", "editor", "content", "ai",
|
||||
"technology", "publishing", "data", "mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
surface TuiSurface {
|
||||
|
||||
Reference in New Issue
Block a user