Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions app/src/ai/facts/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use warpui::{Entity, EntityId, ModelContext, SingletonEntity, ViewHandle, Window

use crate::PaneViewLocator;
use crate::ai::facts::AIFactView;
use crate::pane_group::{AIFactPane, PaneContent};
use crate::pane_group::{AIFactPane, PaneContent, PaneId};

/// Singleton model to manage state of AI fact panes across multiple windows
/// (where only one AI fact pane can exist per window). Specifically:
/// Singleton model to manage state of AI fact panes across multiple windows.
/// Specifically:
/// - Maintains AI fact view handles to preserve state when panes are hidden
/// - Tracks currently open AI fact panes and their location
#[derive(Default)]
Expand Down Expand Up @@ -68,9 +68,53 @@ impl AIFactManager {
}
}

pub fn deregister_pane(&mut self, window_id: &WindowId, _ctx: &mut ModelContext<Self>) {
/// Registers `pane` as transferred into `window_id`, preserving the
/// invariant that at most one AI fact pane is tracked per window. If
/// `window_id` already has a *different* live pane registered, the
/// existing registration is left untouched and its locator is returned
/// so the caller can reconcile the collision (the transferred pane must
/// be discarded and the existing one kept). `None` means there was no
/// collision -- the slot was empty, or already pointed at this exact
/// pane -- and the transferred pane is now the registered one.
pub fn register_transferred_pane(
&mut self,
pane: &AIFactPane,
pane_group_id: EntityId,
window_id: WindowId,
_ctx: &mut ModelContext<Self>,
) -> Option<PaneViewLocator> {
let incoming = PaneViewLocator {
pane_group_id,
pane_id: pane.id(),
};
let Some(data) = self.panes.get_mut(&window_id) else {
log::warn!("AI fact view should already exist for AI fact pane");
return None;
};
match data.locator {
Some(existing) if existing != incoming => Some(existing),
_ => {
data.locator = Some(incoming);
None
}
}
}

pub fn deregister_pane(
&mut self,
window_id: &WindowId,
pane_group_id: EntityId,
pane_id: PaneId,
_ctx: &mut ModelContext<Self>,
) {
if let Some(data) = self.panes.get_mut(window_id) {
data.locator = None;
let locator = PaneViewLocator {
pane_group_id,
pane_id,
};
if data.locator == Some(locator) {
data.locator = None;
}
}
}
}
Expand Down
283 changes: 279 additions & 4 deletions app/src/pane_group/mod.rs

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion app/src/pane_group/pane/ai_fact_pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@ impl PaneContent for AIFactPane {
ctx.unsubscribe_to_view(&self.view);

// Always deregister from AIFactManager - it will be re-registered on attach if restored
let pane_id = self.id();
let pane_group_id = ctx.view_id();
let window_id = ctx.window_id();
AIFactManager::handle(ctx).update(ctx, |manager, ctx| {
manager.deregister_pane(&window_id, ctx);
manager.deregister_pane(&window_id, pane_group_id, pane_id, ctx);
});
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/pane_group/pane/settings_pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl SettingsPane {
Self::from_view(view, ctx)
}

fn settings_view(&self, ctx: &AppContext) -> ViewHandle<SettingsView> {
pub(crate) fn settings_view(&self, ctx: &AppContext) -> ViewHandle<SettingsView> {
self.view.as_ref(ctx).child(ctx)
}
}
Expand Down
33 changes: 33 additions & 0 deletions app/src/settings_view/pane_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use warpui::{Entity, EntityId, ModelContext, SingletonEntity, ViewHandle, Window
use super::SettingsView;
use crate::PaneViewLocator;
use crate::pane_group::{PaneContent, PaneId, SettingsPane};

struct SettingsPaneData {
locator: Option<PaneViewLocator>,
settings_view: ViewHandle<SettingsView>,
Expand Down Expand Up @@ -67,6 +68,38 @@ impl SettingsPaneManager {
}
}

/// Registers `pane` as transferred into `window_id`, preserving the
/// invariant that at most one Settings pane is tracked per window. If
/// `window_id` already has a *different* live pane registered, the
/// existing registration is left untouched and its locator is returned
/// so the caller can reconcile the collision (the transferred pane must
/// be discarded and the existing one kept). `None` means there was no
/// collision -- the slot was empty, or already pointed at this exact
/// pane -- and the transferred pane is now the registered one.
pub fn register_transferred_pane(
&mut self,
pane: &SettingsPane,
pane_group_id: EntityId,
window_id: WindowId,
_ctx: &mut ModelContext<Self>,
) -> Option<PaneViewLocator> {
let incoming = PaneViewLocator {
pane_group_id,
pane_id: pane.id(),
};
let Some(data) = self.panes.get_mut(&window_id) else {
log::warn!("Settings view should already exist for settings pane");
return None;
};
match data.locator {
Some(existing) if existing != incoming => Some(existing),
_ => {
data.locator = Some(incoming);
None
}
}
}

pub fn deregister_pane(
&mut self,
window_id: &WindowId,
Expand Down
18 changes: 18 additions & 0 deletions app/src/workspace/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,21 @@ pub enum AutoCloudHandoffTrigger {

#[derive(Debug, Clone)]
pub enum WorkspaceAction {
/// Reconciles a Settings/AI-fact pane transfer that collided with a live
/// pane of the same kind already in this window (Warp enforces at most
/// one of each per window): discards the just-transferred `discard`
/// pane and focuses the pre-existing `keep` pane. Always dispatched, as
/// a *self*-targeted deferred action, from
/// `PaneGroup::rehome_pane_event_subscription`; see that method's doc
/// comment for why. Self-targeting (rather than reaching this from an
/// ancestor-chain dispatch) is what lets the handler safely touch the
/// just-transferred pane group -- it no longer depends on that pane
/// group's render-time parent link in this window, which isn't
/// registered until the next render pass.
DiscardDuplicateTransferredPane {
keep: PaneViewLocator,
discard: PaneViewLocator,
},
ActivateTab(usize),
ActivatePrevTab,
ActivateNextTab,
Expand Down Expand Up @@ -1251,6 +1266,9 @@ impl WorkspaceAction {
#[cfg(feature = "local_fs")]
FileDeleted { .. } => false, // File deletion doesn't change workspace state
OpenEnvironmentManagementPane => false,
// Internal bookkeeping dispatched by `PaneGroup::rehome_pane_event_subscription`,
// not a user action; doesn't reflect a change worth persisting.
DiscardDuplicateTransferredPane { .. } => false,
#[cfg(target_os = "linux")]
DismissWaylandCrashRecoveryBannerAndOpenLink => false,
#[cfg(target_family = "wasm")]
Expand Down
Loading