use uuid::Uuid;use warp_errors::report_error;use warpui::r#async::SpawnedFutureHandle;use warpui::{ AppContext, ClosedWindowData, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity, ViewHandle, WeakViewHandle, WindowId,}; use super::UndoCloseSettings;use super::settings::UndoCloseSettingsChangedEvent;use crate::ai::active_agent_views_model::ActiveAgentViewsModel;use crate::ai::blocklist::BlocklistAIHistoryModel;use crate::pane_group::{PaneGroup, PaneId};use crate::send_telemetry_from_app_ctx;use crate::server::telemetry::{TelemetryEvent, UndoCloseItemType};use crate::tab::TabData;use crate::window_settings::WindowSettings;use crate::workspace::Workspace; /// A unique identifier for an item in the undo close stack.#[derive(Debug, Copy, Clone, PartialEq, Eq)]struct ItemId(Uuid); impl ItemId { /// Constructs a new ItemId. fn new() -> Self { Self(Uuid::new_v4()) }} /// Data for an item in the undo close stack.struct UndoData { closed_item: ClosedItem, expiry_data: ExpiryData,} /// Data needed to handle expiration for items in the undo close stack.struct ExpiryData { id: ItemId, task_handle: SpawnedFutureHandle,} impl std::ops::Drop for ExpiryData { fn drop(&mut self) { // Make sure we abort the expiry task when we drop the expiry data. self.task_handle.abort(); }} /// Data needed to restore a closed pane.pub(super) struct PaneData { /// The pane ID - content is retrieved from the pane group during restoration pane_id: PaneId, /// Reference to the pane group that contained this pane pane_group: WeakViewHandle<PaneGroup>,} /// An item in the undo close stack which can be re-opened.pub enum ClosedItem { Window(Box<ClosedWindowData>), Tab { workspace: WeakViewHandle<Workspace>, tab_index: usize, data: TabData, }, Pane { data: PaneData, },} impl ClosedItem { fn discard(self, ctx: &mut ModelContext<UndoCloseStack>) { let history_model = BlocklistAIHistoryModel::handle(ctx); match self { ClosedItem::Window(data) => { let ClosedWindowData { window_id, .. } = *data; ActiveAgentViewsModel::handle(ctx).update(ctx, |model, ctx| { model.remove_focused_state_for_window(window_id, ctx); }); if let Some(workspace) = window_workspace(window_id, ctx) { workspace.update(ctx, |workspace, ctx| { for pane_group in workspace.tab_views() { // Mark conversations from all terminal panes in each tab Self::mark_conversations_historical_for_pane_group( pane_group, &history_model, ctx, ); Self::clean_up_pane_group(pane_group, ctx); } }); } } ClosedItem::Tab { data, .. } => { // Mark conversations from all terminal panes in the tab Self::mark_conversations_historical_for_pane_group( &data.pane_group, &history_model, ctx, ); Self::clean_up_pane_group(&data.pane_group, ctx); } ClosedItem::Pane { data } => { ctx.emit(UndoCloseStackEvent::DiscardPane(data.pane_id)); } } } /// Marks conversations as historical for all terminal panes in a pane group so they remain searchable. /// Historical conversations consist of non-live conversations that were read from disk on startup, /// and conversations (recorded here) that were live this session but have now been cleared. fn mark_conversations_historical_for_pane_group( pane_group: &ViewHandle<PaneGroup>, history_model: &ModelHandle<BlocklistAIHistoryModel>, ctx: &mut AppContext, ) { // Check if the window and view still exist before attempting to read let window_id = pane_group.window_id(ctx); let view_id = pane_group.id(); if ctx.view_with_id::<PaneGroup>(window_id, view_id).is_some() { let terminal_view_ids: Vec<EntityId> = pane_group.read(ctx, |pg, ctx| { pg.terminal_pane_ids() .filter_map(|pane_id| { pg.terminal_view_from_pane_id(pane_id, ctx) .map(|terminal_view| terminal_view.id()) }) .collect() }); for terminal_view_id in terminal_view_ids { history_model.update(ctx, |history_model, _| { history_model .mark_conversations_historical_for_terminal_surface(terminal_view_id); }); } } } fn clean_up_pane_group(pane_group: &ViewHandle<PaneGroup>, ctx: &mut AppContext) { let window_id = pane_group.window_id(ctx); if !ctx.is_window_open(window_id) { return; } pane_group.update(ctx, |pane_group, ctx| { pane_group.clean_up_panes(ctx); }); }} pub enum UndoCloseStackEvent { DiscardPane(PaneId),} /// A stack of closed items which can be re-opened in LIFO order.pub struct UndoCloseStack { stack: Vec<UndoData>,} impl UndoCloseStack { /// Constructs a new undo close stack. pub fn new(ctx: &mut ModelContext<Self>) -> Self { ctx.subscribe_to_model(&UndoCloseSettings::handle(ctx), |me, _, event, ctx| { me.handle_settings_event(event, ctx); }); Self { stack: Default::default(), } } /// Returns whether or not the stack is empty. #[cfg_attr(not(target_os = "macos"), allow(dead_code))] pub fn is_empty(&self) -> bool { self.stack.is_empty() } /// Returns true only if the pane group is present in the undo close stack as part of a closed tab. pub fn is_pane_group_tab_in_stack(&self, pane_group_id: EntityId) -> bool { self.stack .iter() .any(|undo_data| matches!(&undo_data.closed_item, ClosedItem::Tab { data, .. } if data.pane_group.id() == pane_group_id)) } /// Discards a pane group from the undo close stack early. pub fn discard_pane_group_parent( &mut self, pane_group_id: EntityId, ctx: &mut ModelContext<Self>, ) { if let Some(pos) = self .stack .iter() .position(|undo_data| match &undo_data.closed_item { ClosedItem::Tab { data, .. } => data.pane_group.id() == pane_group_id, ClosedItem::Pane { data } => data.pane_group.id() == pane_group_id, _ => false, }) { let removed_item = self.stack.remove(pos); removed_item.expiry_data.task_handle.abort(); removed_item.closed_item.discard(ctx); } } /// Handles a window being closed, adding the necessary data to the undo /// stack. pub fn handle_window_closed(&mut self, data: ClosedWindowData, ctx: &mut ModelContext<Self>) { self.push_item(ClosedItem::Window(Box::new(data)), ctx); } /// Handles a tab being closed, adding the necessary data to the undo /// stack. pub fn handle_tab_closed( &mut self, workspace: WeakViewHandle<Workspace>, tab_index: usize, data: TabData, ctx: &mut ModelContext<Self>, ) { self.push_item( ClosedItem::Tab { workspace, tab_index, data, }, ctx, ); } /// Handles a pane being closed, adding the necessary data to the undo stack. pub fn handle_pane_closed_by_id( &mut self, pane_group: WeakViewHandle<PaneGroup>, pane_id: PaneId, ctx: &mut ModelContext<Self>, ) { let pane_data = PaneData { pane_id, pane_group, }; self.push_item(ClosedItem::Pane { data: pane_data }, ctx); } /// Undoes the last close action in the stack, if possible. pub fn undo_close(&mut self, ctx: &mut AppContext) { let Some(UndoData { closed_item, .. }) = self.stack.pop() else { return; }; match closed_item { ClosedItem::Window(data) => { send_telemetry_from_app_ctx!( TelemetryEvent::UndoClose { item_type: UndoCloseItemType::Window, }, ctx ); let window_id = data.window_id; let (background_blur_radius_pixels, background_backdrop) = { let window_settings = WindowSettings::as_ref(ctx); ( Some(*window_settings.background_blur_radius), *window_settings.background_backdrop, ) }; ctx.reopen_closed_window(*data, background_blur_radius_pixels, background_backdrop); if let Some(workspace) = window_workspace(window_id, ctx) { workspace.update(ctx, |workspace, ctx| { workspace.handle_reopen(ctx); }); } // Make sure we update our session restoration state now that the // window has been reopened. ctx.dispatch_global_action("workspace:save_app", &()); } ClosedItem::Tab { workspace, tab_index, data, } => { if let Some(workspace) = workspace.upgrade(ctx) { send_telemetry_from_app_ctx!( TelemetryEvent::UndoClose { item_type: UndoCloseItemType::Tab, }, ctx ); workspace.update(ctx, |workspace, ctx| { workspace.restore_closed_tab(tab_index, data, ctx); }); ctx.windows() .show_window_and_focus_app(workspace.window_id(ctx)); } // Make sure we update our session restoration state now that the // tab has been reopened. ctx.dispatch_global_action("workspace:save_app", &()); } ClosedItem::Pane { data } => { if let Some(pane_group) = data.pane_group.upgrade(ctx) { let pane_id = data.pane_id; let window_id = pane_group.window_id(ctx); let pane_group_id = pane_group.id(); let restored = pane_group.update(ctx, |pane_group, ctx| { pane_group.restore_closed_pane(pane_id, ctx) }); if restored { send_telemetry_from_app_ctx!( TelemetryEvent::UndoClose { item_type: UndoCloseItemType::Pane, }, ctx ); // Focus the window first ctx.windows().show_window_and_focus_app(window_id); // Now properly focus the restored pane by activating its tab and focusing the pane if let Some(workspace) = window_workspace(window_id, ctx) { workspace.update(ctx, |workspace, ctx| { let locator = crate::workspace::PaneViewLocator { pane_group_id, pane_id, }; workspace.focus_pane(locator, ctx); }); } ctx.dispatch_global_action("workspace:save_app", &()); } } } } } /// Handles a change to the undo close settings. fn handle_settings_event( &mut self, event: &UndoCloseSettingsChangedEvent, ctx: &mut ModelContext<Self>, ) { match event { UndoCloseSettingsChangedEvent::UndoCloseEnabled { .. } => { let settings = UndoCloseSettings::as_ref(ctx); if !*settings.enabled { for undo_data in self.stack.drain(..) { undo_data.closed_item.discard(ctx); } } } UndoCloseSettingsChangedEvent::UndoCloseGracePeriod { .. } => {} } } /// Pushes a new item onto the stack. fn push_item(&mut self, closed_item: ClosedItem, ctx: &mut ModelContext<Self>) { let settings = UndoCloseSettings::as_ref(ctx); if !*settings.enabled { closed_item.discard(ctx); return; } let id = ItemId::new(); let grace_period = *settings.grace_period; let task_handle = ctx.spawn_abortable( warpui::r#async::Timer::after(grace_period), move |me, _, ctx| { let initial_len = me.stack.len(); if let Some(pos) = me.stack.iter().position(|item| item.expiry_data.id == id) { let removed_item = me.stack.remove(pos); removed_item.closed_item.discard(ctx); } // Log errors if the expired item was not found or multiple items were found if me.stack.len() == initial_len { report_error!("Undo close expiry task did not find item in stack!"); } else if me.stack.len() < initial_len - 1 { report_error!("Undo close expiry task found multiple matching items in stack!"); } else { log::debug!("Removed expired item from undo stack"); } }, |_, _| {}, ); self.stack.push(UndoData { closed_item, expiry_data: ExpiryData { id, task_handle }, }) }} /// Find the root [`Workspace`] view for a window.fn window_workspace(window_id: WindowId, ctx: &mut AppContext) -> Option<ViewHandle<Workspace>> { ctx.views_of_type::<Workspace>(window_id) .and_then(|views| views.first().cloned())} impl Entity for UndoCloseStack { type Event = UndoCloseStackEvent;} impl SingletonEntity for UndoCloseStack {}
use uuid::Uuid;
use warp_errors::report_error;
use warpui::r#async::SpawnedFutureHandle;
use warpui::{
AppContext, ClosedWindowData, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity,
ViewHandle, WeakViewHandle, WindowId,
};
use super::UndoCloseSettings;
use super::settings::UndoCloseSettingsChangedEvent;
use crate::ai::active_agent_views_model::ActiveAgentViewsModel;
use crate::ai::blocklist::BlocklistAIHistoryModel;
use crate::pane_group::{PaneGroup, PaneId};
use crate::send_telemetry_from_app_ctx;
use crate::server::telemetry::{TelemetryEvent, UndoCloseItemType};
use crate::tab::TabData;
use crate::window_settings::WindowSettings;
use crate::workspace::Workspace;
/// A unique identifier for an item in the undo close stack.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct ItemId(Uuid);
impl ItemId {
/// Constructs a new ItemId.
fn new() -> Self {
Self(Uuid::new_v4())
}
}
/// Data for an item in the undo close stack.
struct UndoData {
closed_item: ClosedItem,
expiry_data: ExpiryData,
}
/// Data needed to handle expiration for items in the undo close stack.
struct ExpiryData {
id: ItemId,
task_handle: SpawnedFutureHandle,
}
impl std::ops::Drop for ExpiryData {
fn drop(&mut self) {
// Make sure we abort the expiry task when we drop the expiry data.
self.task_handle.abort();
}
}
/// Data needed to restore a closed pane.
pub(super) struct PaneData {
/// The pane ID - content is retrieved from the pane group during restoration
pane_id: PaneId,
/// Reference to the pane group that contained this pane
pane_group: WeakViewHandle<PaneGroup>,
}
/// An item in the undo close stack which can be re-opened.
pub enum ClosedItem {
Window(Box<ClosedWindowData>),
Tab {
workspace: WeakViewHandle<Workspace>,
tab_index: usize,
data: TabData,
},
Pane {
data: PaneData,
},
}
impl ClosedItem {
fn discard(self, ctx: &mut ModelContext<UndoCloseStack>) {
let history_model = BlocklistAIHistoryModel::handle(ctx);
match self {
ClosedItem::Window(data) => {
let ClosedWindowData { window_id, .. } = *data;
ActiveAgentViewsModel::handle(ctx).update(ctx, |model, ctx| {
model.remove_focused_state_for_window(window_id, ctx);
});
if let Some(workspace) = window_workspace(window_id, ctx) {
workspace.update(ctx, |workspace, ctx| {
for pane_group in workspace.tab_views() {
// Mark conversations from all terminal panes in each tab
Self::mark_conversations_historical_for_pane_group(
pane_group,
&history_model,
ctx,
);
Self::clean_up_pane_group(pane_group, ctx);
}
});
}
}
ClosedItem::Tab { data, .. } => {
// Mark conversations from all terminal panes in the tab
Self::mark_conversations_historical_for_pane_group(
&data.pane_group,
&history_model,
ctx,
);
Self::clean_up_pane_group(&data.pane_group, ctx);
}
ClosedItem::Pane { data } => {
ctx.emit(UndoCloseStackEvent::DiscardPane(data.pane_id));
}
}
}
/// Marks conversations as historical for all terminal panes in a pane group so they remain searchable.
/// Historical conversations consist of non-live conversations that were read from disk on startup,
/// and conversations (recorded here) that were live this session but have now been cleared.
fn mark_conversations_historical_for_pane_group(
pane_group: &ViewHandle<PaneGroup>,
history_model: &ModelHandle<BlocklistAIHistoryModel>,
ctx: &mut AppContext,
) {
// Check if the window and view still exist before attempting to read
let window_id = pane_group.window_id(ctx);
let view_id = pane_group.id();
if ctx.view_with_id::<PaneGroup>(window_id, view_id).is_some() {
let terminal_view_ids: Vec<EntityId> = pane_group.read(ctx, |pg, ctx| {
pg.terminal_pane_ids()
.filter_map(|pane_id| {
pg.terminal_view_from_pane_id(pane_id, ctx)
.map(|terminal_view| terminal_view.id())
})
.collect()
});
for terminal_view_id in terminal_view_ids {
history_model.update(ctx, |history_model, _| {
history_model
.mark_conversations_historical_for_terminal_surface(terminal_view_id);
});
}
}
}
fn clean_up_pane_group(pane_group: &ViewHandle<PaneGroup>, ctx: &mut AppContext) {
let window_id = pane_group.window_id(ctx);
if !ctx.is_window_open(window_id) {
return;
}
pane_group.update(ctx, |pane_group, ctx| {
pane_group.clean_up_panes(ctx);
});
}
}
pub enum UndoCloseStackEvent {
DiscardPane(PaneId),
}
/// A stack of closed items which can be re-opened in LIFO order.
pub struct UndoCloseStack {
stack: Vec<UndoData>,
}
impl UndoCloseStack {
/// Constructs a new undo close stack.
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
ctx.subscribe_to_model(&UndoCloseSettings::handle(ctx), |me, _, event, ctx| {
me.handle_settings_event(event, ctx);
});
Self {
stack: Default::default(),
}
}
/// Returns whether or not the stack is empty.
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub fn is_empty(&self) -> bool {
self.stack.is_empty()
}
/// Returns true only if the pane group is present in the undo close stack as part of a closed tab.
pub fn is_pane_group_tab_in_stack(&self, pane_group_id: EntityId) -> bool {
self.stack
.iter()
.any(|undo_data| matches!(&undo_data.closed_item, ClosedItem::Tab { data, .. } if data.pane_group.id() == pane_group_id))
}
/// Discards a pane group from the undo close stack early.
pub fn discard_pane_group_parent(
&mut self,
pane_group_id: EntityId,
ctx: &mut ModelContext<Self>,
) {
if let Some(pos) = self
.stack
.iter()
.position(|undo_data| match &undo_data.closed_item {
ClosedItem::Tab { data, .. } => data.pane_group.id() == pane_group_id,
ClosedItem::Pane { data } => data.pane_group.id() == pane_group_id,
_ => false,
})
{
let removed_item = self.stack.remove(pos);
removed_item.expiry_data.task_handle.abort();
removed_item.closed_item.discard(ctx);
}
}
/// Handles a window being closed, adding the necessary data to the undo
/// stack.
pub fn handle_window_closed(&mut self, data: ClosedWindowData, ctx: &mut ModelContext<Self>) {
self.push_item(ClosedItem::Window(Box::new(data)), ctx);
}
/// Handles a tab being closed, adding the necessary data to the undo
/// stack.
pub fn handle_tab_closed(
&mut self,
workspace: WeakViewHandle<Workspace>,
tab_index: usize,
data: TabData,
ctx: &mut ModelContext<Self>,
) {
self.push_item(
ClosedItem::Tab {
workspace,
tab_index,
data,
},
ctx,
);
}
/// Handles a pane being closed, adding the necessary data to the undo stack.
pub fn handle_pane_closed_by_id(
&mut self,
pane_group: WeakViewHandle<PaneGroup>,
pane_id: PaneId,
ctx: &mut ModelContext<Self>,
) {
let pane_data = PaneData {
pane_id,
pane_group,
};
self.push_item(ClosedItem::Pane { data: pane_data }, ctx);
}
/// Undoes the last close action in the stack, if possible.
pub fn undo_close(&mut self, ctx: &mut AppContext) {
let Some(UndoData { closed_item, .. }) = self.stack.pop() else {
return;
};
match closed_item {
ClosedItem::Window(data) => {
send_telemetry_from_app_ctx!(
TelemetryEvent::UndoClose {
item_type: UndoCloseItemType::Window,
},
ctx
);
let window_id = data.window_id;
let (background_blur_radius_pixels, background_backdrop) = {
let window_settings = WindowSettings::as_ref(ctx);
(
Some(*window_settings.background_blur_radius),
*window_settings.background_backdrop,
)
};
ctx.reopen_closed_window(*data, background_blur_radius_pixels, background_backdrop);
if let Some(workspace) = window_workspace(window_id, ctx) {
workspace.update(ctx, |workspace, ctx| {
workspace.handle_reopen(ctx);
});
}
// Make sure we update our session restoration state now that the
// window has been reopened.
ctx.dispatch_global_action("workspace:save_app", &());
}
ClosedItem::Tab {
workspace,
tab_index,
data,
} => {
if let Some(workspace) = workspace.upgrade(ctx) {
send_telemetry_from_app_ctx!(
TelemetryEvent::UndoClose {
item_type: UndoCloseItemType::Tab,
},
ctx
);
workspace.update(ctx, |workspace, ctx| {
workspace.restore_closed_tab(tab_index, data, ctx);
});
ctx.windows()
.show_window_and_focus_app(workspace.window_id(ctx));
}
// Make sure we update our session restoration state now that the
// tab has been reopened.
ctx.dispatch_global_action("workspace:save_app", &());
}
ClosedItem::Pane { data } => {
if let Some(pane_group) = data.pane_group.upgrade(ctx) {
let pane_id = data.pane_id;
let window_id = pane_group.window_id(ctx);
let pane_group_id = pane_group.id();
let restored = pane_group.update(ctx, |pane_group, ctx| {
pane_group.restore_closed_pane(pane_id, ctx)
});
if restored {
send_telemetry_from_app_ctx!(
TelemetryEvent::UndoClose {
item_type: UndoCloseItemType::Pane,
},
ctx
);
// Focus the window first
ctx.windows().show_window_and_focus_app(window_id);
// Now properly focus the restored pane by activating its tab and focusing the pane
if let Some(workspace) = window_workspace(window_id, ctx) {
workspace.update(ctx, |workspace, ctx| {
let locator = crate::workspace::PaneViewLocator {
pane_group_id,
pane_id,
};
workspace.focus_pane(locator, ctx);
});
}
ctx.dispatch_global_action("workspace:save_app", &());
}
}
}
}
}
/// Handles a change to the undo close settings.
fn handle_settings_event(
&mut self,
event: &UndoCloseSettingsChangedEvent,
ctx: &mut ModelContext<Self>,
) {
match event {
UndoCloseSettingsChangedEvent::UndoCloseEnabled { .. } => {
let settings = UndoCloseSettings::as_ref(ctx);
if !*settings.enabled {
for undo_data in self.stack.drain(..) {
undo_data.closed_item.discard(ctx);
}
}
}
UndoCloseSettingsChangedEvent::UndoCloseGracePeriod { .. } => {}
}
}
/// Pushes a new item onto the stack.
fn push_item(&mut self, closed_item: ClosedItem, ctx: &mut ModelContext<Self>) {
let settings = UndoCloseSettings::as_ref(ctx);
if !*settings.enabled {
closed_item.discard(ctx);
return;
}
let id = ItemId::new();
let grace_period = *settings.grace_period;
let task_handle = ctx.spawn_abortable(
warpui::r#async::Timer::after(grace_period),
move |me, _, ctx| {
let initial_len = me.stack.len();
if let Some(pos) = me.stack.iter().position(|item| item.expiry_data.id == id) {
let removed_item = me.stack.remove(pos);
removed_item.closed_item.discard(ctx);
}
// Log errors if the expired item was not found or multiple items were found
if me.stack.len() == initial_len {
report_error!("Undo close expiry task did not find item in stack!");
} else if me.stack.len() < initial_len - 1 {
report_error!("Undo close expiry task found multiple matching items in stack!");
} else {
log::debug!("Removed expired item from undo stack");
}
},
|_, _| {},
);
self.stack.push(UndoData {
closed_item,
expiry_data: ExpiryData { id, task_handle },
})
}
}
/// Find the root [`Workspace`] view for a window.
fn window_workspace(window_id: WindowId, ctx: &mut AppContext) -> Option<ViewHandle<Workspace>> {
ctx.views_of_type::<Workspace>(window_id)
.and_then(|views| views.first().cloned())
}
impl Entity for UndoCloseStack {
type Event = UndoCloseStackEvent;
}
impl SingletonEntity for UndoCloseStack {}