feat(windows): persistance et restauration des fenêtres au redémarrage (#40)
Persiste l'état des fenêtres (layout/position) et le restaure au relancement de l'application. Découpage hexagonal : - domaine : modèle et port d'état des fenêtres (layout, ports) - application : use cases de persistance/restauration - infrastructure : store window_state (adapter de persistance) - présentation : câblage app-tauri (state, commands, lib) Couvert par des tests ciblés domaine/application/infrastructure. Depend de #39 (fermeture des fenêtres auxiliaires). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1025,3 +1025,119 @@ impl Workspace {
|
||||
Ok(Self { windows })
|
||||
}
|
||||
}
|
||||
|
||||
/// Current persisted schema version for OS window state.
|
||||
pub const WINDOW_STATE_SNAPSHOT_VERSION: u32 = 1;
|
||||
|
||||
/// Persisted kind of an IdeA OS window.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PersistedWindowKind {
|
||||
/// The primary IdeA window.
|
||||
Main,
|
||||
/// A detached project view window.
|
||||
View,
|
||||
}
|
||||
|
||||
/// Physical top-left position of a window or monitor.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersistedWindowPosition {
|
||||
/// X coordinate in physical pixels.
|
||||
pub x: i32,
|
||||
/// Y coordinate in physical pixels.
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
/// Physical size of a window or monitor.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersistedWindowSize {
|
||||
/// Width in physical pixels.
|
||||
pub width: u32,
|
||||
/// Height in physical pixels.
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
/// Best-effort monitor descriptor captured with a window.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersistedMonitorState {
|
||||
/// Human-readable monitor name when the platform exposes one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// Scale factor reported by the windowing system.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scale_factor: Option<f64>,
|
||||
/// Monitor origin in the global desktop coordinate space.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub position: Option<PersistedWindowPosition>,
|
||||
/// Monitor physical size.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<PersistedWindowSize>,
|
||||
}
|
||||
|
||||
/// Persisted state of one IdeA webview window.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersistedWindowState {
|
||||
/// Stable Tauri label. For detached views this is
|
||||
/// `view-<panel>-<projectId-simple>`.
|
||||
pub label: String,
|
||||
/// Window kind.
|
||||
pub kind: PersistedWindowKind,
|
||||
/// Detached view panel id. Present only for [`PersistedWindowKind::View`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub panel: Option<String>,
|
||||
/// Detached view project id. Present only for [`PersistedWindowKind::View`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub project_id: Option<crate::ids::ProjectId>,
|
||||
/// App URL loaded in the window.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
/// Whether the window was visible.
|
||||
pub visible: bool,
|
||||
/// Whether the window was maximized.
|
||||
pub maximized: bool,
|
||||
/// Whether the window was fullscreen.
|
||||
pub fullscreen: bool,
|
||||
/// Outer window position.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub outer_position: Option<PersistedWindowPosition>,
|
||||
/// Outer window size.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub outer_size: Option<PersistedWindowSize>,
|
||||
/// Current monitor at snapshot time.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub monitor: Option<PersistedMonitorState>,
|
||||
/// Optional focus recency, left unset until focus tracking exists.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_focused_at: Option<u64>,
|
||||
}
|
||||
|
||||
/// Persisted snapshot of the IdeA OS windows.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WindowStateSnapshot {
|
||||
/// Schema version.
|
||||
pub version: u32,
|
||||
/// Captured windows.
|
||||
pub windows: Vec<PersistedWindowState>,
|
||||
}
|
||||
|
||||
impl WindowStateSnapshot {
|
||||
/// Builds a versioned snapshot from captured windows.
|
||||
#[must_use]
|
||||
pub fn new(windows: Vec<PersistedWindowState>) -> Self {
|
||||
Self {
|
||||
version: WINDOW_STATE_SNAPSHOT_VERSION,
|
||||
windows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WindowStateSnapshot {
|
||||
fn default() -> Self {
|
||||
Self::new(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,7 +168,9 @@ pub use git::GitRepository;
|
||||
|
||||
pub use layout::{
|
||||
Direction, GridCell, GridContainer, LayoutError, LayoutNode, LayoutTree, LeafCell,
|
||||
SplitContainer, Tab, WeightedChild, Window, Workspace,
|
||||
PersistedMonitorState, PersistedWindowKind, PersistedWindowPosition, PersistedWindowSize,
|
||||
PersistedWindowState, SplitContainer, Tab, WeightedChild, Window, WindowStateSnapshot,
|
||||
Workspace, WINDOW_STATE_SNAPSHOT_VERSION,
|
||||
};
|
||||
|
||||
pub use events::{DomainEvent, OrchestrationSource};
|
||||
@ -201,4 +203,5 @@ pub use ports::{
|
||||
OutputStream, PermissionStore, PreparedContext, ProcessError, ProcessSpawner, ProfileStore,
|
||||
ProjectStore, PtyError, PtyHandle, PtyPort, RemoteError, RemoteHost, RemotePath, RuntimeError,
|
||||
ScheduledTask, Scheduler, SpawnSpec, SprintStore, SprintStoreError, StoreError, TemplateStore,
|
||||
WindowStateStore,
|
||||
};
|
||||
|
||||
@ -1417,6 +1417,26 @@ pub trait ProjectStore: Send + Sync {
|
||||
async fn load_workspace(&self) -> Result<crate::layout::Workspace, StoreError>;
|
||||
}
|
||||
|
||||
/// Machine-local persistence of open OS window state.
|
||||
#[async_trait]
|
||||
pub trait WindowStateStore: Send + Sync {
|
||||
/// Saves the latest open-window snapshot.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`StoreError`] on persistence failure.
|
||||
async fn save_window_state(
|
||||
&self,
|
||||
snapshot: &crate::layout::WindowStateSnapshot,
|
||||
) -> Result<(), StoreError>;
|
||||
|
||||
/// Loads the latest open-window snapshot.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`StoreError`] on unexpected I/O or deserialisation failure. A missing
|
||||
/// snapshot is represented as an empty default snapshot.
|
||||
async fn load_window_state(&self) -> Result<crate::layout::WindowStateSnapshot, StoreError>;
|
||||
}
|
||||
|
||||
/// CRUD for the configured [`AgentProfile`]s in the global IDE store
|
||||
/// (`profiles.json`, ARCHITECTURE §9.2). Profiles are the *data* that drives the
|
||||
/// single generic [`AgentRuntime`] adapter (Open/Closed).
|
||||
|
||||
Reference in New Issue
Block a user