first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use session_sharing_protocol::common::SessionId;
|
||||
use uuid::Uuid;
|
||||
use warpui::{SingletonEntity, ViewContext};
|
||||
|
||||
use super::{apply_hidden_child_agent_task_context, HiddenChildAgentTaskContext};
|
||||
use crate::ai::agent::conversation::{AIConversation, AIConversationId};
|
||||
use crate::ai::blocklist::agent_view::AgentViewEntryOrigin;
|
||||
use crate::ai::blocklist::BlocklistAIHistoryModel;
|
||||
use crate::ai::restored_conversations::RestoredAgentConversations;
|
||||
use crate::pane_group::{
|
||||
AmbientAgentViewModelHandleExt, PaneGroup, PaneId, TerminalPane, TerminalViewResources,
|
||||
};
|
||||
use crate::terminal::shared_session::IsSharedSessionCreator;
|
||||
use crate::terminal::view::load_ai_conversation::{
|
||||
RestoreConversationEntryBehavior, RestoredAIConversation,
|
||||
};
|
||||
|
||||
impl PaneGroup {
|
||||
/// Lazily restores hidden child panes for the given parent conversation.
|
||||
///
|
||||
/// Unlike the old startup sweep, this runs only when the parent agent view
|
||||
/// is actually restored or entered. Children that already belong to some
|
||||
/// other pane or tab are left alone.
|
||||
pub(in crate::pane_group) fn restore_missing_child_agent_panes_for_parent(
|
||||
&mut self,
|
||||
parent_conversation_id: AIConversationId,
|
||||
parent_pane_id: PaneId,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let child_ids = BlocklistAIHistoryModel::as_ref(ctx)
|
||||
.child_conversation_ids_of(&parent_conversation_id)
|
||||
.to_vec();
|
||||
|
||||
for child_id in child_ids {
|
||||
if self
|
||||
.child_agent_panes
|
||||
.get(&child_id)
|
||||
.is_some_and(|pane_id| self.has_pane_id(*pane_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.is_conversation_owned_outside_pane(child_id, parent_pane_id, ctx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let child_conversation = BlocklistAIHistoryModel::as_ref(ctx)
|
||||
.conversation(&child_id)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
RestoredAgentConversations::handle(ctx)
|
||||
.update(ctx, |store, _| store.take_conversation(&child_id))
|
||||
});
|
||||
let Some(child_conversation) = child_conversation else {
|
||||
log::warn!("Child conversation {child_id:?} not found in memory or restored store");
|
||||
continue;
|
||||
};
|
||||
|
||||
self.create_hidden_child_agent_pane(child_conversation, parent_pane_id, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores hidden child panes if this terminal pane is already showing a
|
||||
/// fullscreen agent view. This covers restored or replaced panes whose
|
||||
/// terminal view entered agent view before pane-group attachment finished.
|
||||
pub(in crate::pane_group) fn restore_missing_child_agent_panes_for_terminal_pane_if_needed(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(terminal_pane_id) = pane_id.as_terminal_pane_id() else {
|
||||
return;
|
||||
};
|
||||
let Some(parent_conversation_id) = self
|
||||
.terminal_view_from_pane_id(terminal_pane_id, ctx)
|
||||
.and_then(|terminal_view| {
|
||||
let terminal_view = terminal_view.as_ref(ctx);
|
||||
let controller = terminal_view.agent_view_controller().as_ref(ctx);
|
||||
if controller.is_fullscreen() {
|
||||
controller.agent_view_state().active_conversation_id()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.restore_missing_child_agent_panes_for_parent(
|
||||
parent_conversation_id,
|
||||
terminal_pane_id.into(),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensures `child_conversation_id` has a hidden child pane if it still
|
||||
/// belongs under a parent conversation in this pane group.
|
||||
///
|
||||
/// Returns true if the conversation is already reachable through an
|
||||
/// existing pane or if lazy restoration successfully materialized the child
|
||||
/// pane.
|
||||
pub(in crate::pane_group) fn ensure_hidden_child_agent_pane_for_conversation(
|
||||
&mut self,
|
||||
child_conversation_id: AIConversationId,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if self
|
||||
.child_agent_panes
|
||||
.get(&child_conversation_id)
|
||||
.is_some_and(|pane_id| self.has_pane_id(*pane_id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
let parent_conversation_id =
|
||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
|
||||
history_model
|
||||
.conversation(&child_conversation_id)
|
||||
.and_then(|conversation| {
|
||||
history_model.resolved_parent_conversation_id_for_conversation(conversation)
|
||||
})
|
||||
.or_else(|| {
|
||||
RestoredAgentConversations::handle(ctx).read(ctx, |store, _| {
|
||||
store.get_conversation(&child_conversation_id).and_then(
|
||||
|conversation| {
|
||||
history_model.resolved_parent_conversation_id_for_conversation(
|
||||
conversation,
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let Some(parent_conversation_id) = parent_conversation_id else {
|
||||
return self
|
||||
.terminal_view_id_for_owned_conversation(child_conversation_id, ctx)
|
||||
.is_some();
|
||||
};
|
||||
|
||||
let child_owner_terminal_view_id =
|
||||
self.terminal_view_id_for_owned_conversation(child_conversation_id, ctx);
|
||||
let Some(parent_pane_id) = self.pane_id_for_owned_conversation(parent_conversation_id, ctx)
|
||||
else {
|
||||
return child_owner_terminal_view_id.is_some();
|
||||
};
|
||||
|
||||
if self.is_conversation_owned_outside_pane(child_conversation_id, parent_pane_id, ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.restore_missing_child_agent_panes_for_parent(
|
||||
parent_conversation_id,
|
||||
parent_pane_id,
|
||||
ctx,
|
||||
);
|
||||
|
||||
self.child_agent_panes
|
||||
.get(&child_conversation_id)
|
||||
.is_some_and(|pane_id| self.has_pane_id(*pane_id))
|
||||
|| self.is_conversation_owned_outside_pane(child_conversation_id, parent_pane_id, ctx)
|
||||
}
|
||||
|
||||
/// Creates a hidden child agent pane for an existing child conversation,
|
||||
/// restoring the conversation and tracking it in `child_agent_panes`.
|
||||
pub(in crate::pane_group) fn create_hidden_child_agent_pane(
|
||||
&mut self,
|
||||
child_conversation: AIConversation,
|
||||
parent_pane_id: PaneId,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let child_id = child_conversation.id();
|
||||
|
||||
// Viewer-side child clicked before `OrchestrationViewerModel`
|
||||
// surfaced a `session_id`: render a loading placeholder; the real
|
||||
// pane gets swapped in by `ensure_shared_session_viewer_child_pane`.
|
||||
if child_conversation.is_viewing_shared_session() {
|
||||
let resources = TerminalViewResources {
|
||||
tips_completed: self.tips_completed.clone(),
|
||||
server_api: self.server_api.clone(),
|
||||
model_event_sender: self.model_event_sender.clone(),
|
||||
};
|
||||
let view_size = Self::estimated_view_bounds(ctx).size();
|
||||
let (loading_view, loading_manager) = Self::create_loading_terminal_manager_and_view(
|
||||
resources,
|
||||
view_size,
|
||||
ctx.window_id(),
|
||||
ctx,
|
||||
);
|
||||
let pane_data = TerminalPane::new(
|
||||
Uuid::new_v4().as_bytes().to_vec(),
|
||||
loading_manager,
|
||||
loading_view.clone(),
|
||||
self.model_event_sender.clone(),
|
||||
ctx,
|
||||
);
|
||||
let new_pane_id = pane_data.terminal_pane_id();
|
||||
if self
|
||||
.attach_child_pane_off_tree(Box::new(pane_data), ctx)
|
||||
.is_none()
|
||||
{
|
||||
log::error!(
|
||||
"create_hidden_child_agent_pane: failed to attach loading placeholder for \
|
||||
viewer-side child {child_id:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the conversation and enter agent view so the pill bar
|
||||
// renders (its gate requires `is_fullscreen()`). The output area
|
||||
// stays a loading spinner because the loading view's
|
||||
// `ConversationTranscriptViewerStatus::Loading` short-circuits
|
||||
// the block list render in `TerminalView::render`.
|
||||
loading_view.update(ctx, |terminal_view, ctx| {
|
||||
terminal_view.restore_conversation_after_view_creation(
|
||||
RestoredAIConversation::new(child_conversation),
|
||||
true,
|
||||
RestoreConversationEntryBehavior::PreserveAgentViewState,
|
||||
ctx,
|
||||
);
|
||||
terminal_view.enter_agent_view(
|
||||
None,
|
||||
Some(child_id),
|
||||
AgentViewEntryOrigin::SharedSessionSelection,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
|
||||
self.child_agent_panes.insert(child_id, new_pane_id.into());
|
||||
return;
|
||||
}
|
||||
|
||||
if child_conversation.is_remote_child() {
|
||||
let Some(task_id) = child_conversation.task_id() else {
|
||||
log::warn!(
|
||||
"Cannot restore remote child conversation {child_id:?} without a task ID"
|
||||
);
|
||||
return;
|
||||
};
|
||||
self.hydrate_task_backed_hidden_child_pane(
|
||||
child_conversation,
|
||||
parent_pane_id,
|
||||
task_id,
|
||||
ctx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let child_task_context =
|
||||
child_conversation
|
||||
.task_id()
|
||||
.map(|task_id| HiddenChildAgentTaskContext {
|
||||
task_id,
|
||||
working_dir: child_conversation
|
||||
.current_working_directory()
|
||||
.or_else(|| child_conversation.initial_working_directory())
|
||||
.map(PathBuf::from),
|
||||
});
|
||||
// Restored hidden child panes don't inherit the host's shared
|
||||
// session — the host's share decision is handled at original
|
||||
// dispatch time, not on subsequent restores.
|
||||
let new_pane_id = self.insert_terminal_pane_hidden_for_child_agent(
|
||||
parent_pane_id,
|
||||
HashMap::new(),
|
||||
IsSharedSessionCreator::No,
|
||||
ctx,
|
||||
);
|
||||
|
||||
if let Some(new_terminal_view) = self.terminal_view_from_pane_id(new_pane_id, ctx) {
|
||||
if let Some(task_context) = child_task_context.as_ref() {
|
||||
apply_hidden_child_agent_task_context(&new_terminal_view, task_context, ctx);
|
||||
}
|
||||
new_terminal_view.update(ctx, |terminal_view, ctx| {
|
||||
terminal_view.restore_conversation_after_view_creation(
|
||||
RestoredAIConversation::new(child_conversation),
|
||||
true,
|
||||
RestoreConversationEntryBehavior::PreserveAgentViewState,
|
||||
ctx,
|
||||
);
|
||||
terminal_view.enter_agent_view(
|
||||
None,
|
||||
Some(child_id),
|
||||
AgentViewEntryOrigin::ChildAgent,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
|
||||
self.child_agent_panes.insert(child_id, new_pane_id.into());
|
||||
} else {
|
||||
log::error!("Failed to get terminal view for child agent pane {child_id:?}");
|
||||
self.discard_pane(new_pane_id.into(), ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes a hidden shared-session viewer pane for a viewer-
|
||||
/// discovered child agent. Triggered by
|
||||
/// `Event::EnsureSharedSessionViewerChildPane`, which
|
||||
/// `OrchestrationViewerModel` emits on the parent's view the first
|
||||
/// time it observes a `session_id` for a child. The new pane gets its
|
||||
/// own `BlocklistAIController` and viewer-side `Network` so child
|
||||
/// traffic doesn't cross the parent's single-stream state.
|
||||
pub(in crate::pane_group) fn ensure_shared_session_viewer_child_pane(
|
||||
&mut self,
|
||||
child_conversation_id: AIConversationId,
|
||||
child_session_id: SessionId,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// Race recovery: a pill click before materialization had a
|
||||
// `session_id` falls through to `create_hidden_child_agent_pane`,
|
||||
// which leaves a loading placeholder in `child_agent_panes`. The
|
||||
// emission gate in `OrchestrationViewerModel` guarantees this
|
||||
// helper runs at most once per child per model lifetime, so any
|
||||
// existing entry must be that fallback — safe to discard.
|
||||
let fallback_was_swapped_anchor = if let Some(prior_pane_id) = self
|
||||
.child_agent_panes
|
||||
.get(&child_conversation_id)
|
||||
.copied()
|
||||
.filter(|pane_id| self.has_pane_id(*pane_id))
|
||||
{
|
||||
let anchor = self.panes.original_pane_for_replacement(prior_pane_id);
|
||||
self.discard_child_agent_pane_for_conversation(child_conversation_id, ctx);
|
||||
anchor
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(child_conversation) = BlocklistAIHistoryModel::as_ref(ctx)
|
||||
.conversation(&child_conversation_id)
|
||||
.cloned()
|
||||
else {
|
||||
log::warn!(
|
||||
"ensure_shared_session_viewer_child_pane: no local conversation {child_conversation_id:?}"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let child_task_id = child_conversation.task_id();
|
||||
|
||||
let resources = TerminalViewResources {
|
||||
tips_completed: self.tips_completed.clone(),
|
||||
server_api: self.server_api.clone(),
|
||||
model_event_sender: self.model_event_sender.clone(),
|
||||
};
|
||||
let view_size = Self::estimated_view_bounds(ctx).size();
|
||||
// Per-child viewer: parent's model already discovers descendants, and
|
||||
// hidden child viewers aren't snapshotted, so `is_cloud_mode` stays
|
||||
// `false` (no `ambient_agent_view_model` needed for snapshot round-trip).
|
||||
let (new_terminal_view, terminal_manager) = Self::create_shared_session_viewer(
|
||||
child_session_id,
|
||||
resources,
|
||||
view_size,
|
||||
false, // enable_orchestration_polling
|
||||
false, // is_cloud_mode
|
||||
ctx,
|
||||
);
|
||||
|
||||
let pane_data = TerminalPane::new(
|
||||
Uuid::new_v4().as_bytes().to_vec(),
|
||||
terminal_manager,
|
||||
new_terminal_view.clone(),
|
||||
self.model_event_sender.clone(),
|
||||
ctx,
|
||||
);
|
||||
let new_pane_id = pane_data.terminal_pane_id();
|
||||
if self
|
||||
.attach_child_pane_off_tree(Box::new(pane_data), ctx)
|
||||
.is_none()
|
||||
{
|
||||
log::error!(
|
||||
"ensure_shared_session_viewer_child_pane: failed to attach pane for conv={child_conversation_id:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
new_terminal_view.update(ctx, |terminal_view, ctx| {
|
||||
terminal_view.suppress_initial_conversation_details_panel_auto_open();
|
||||
terminal_view.restore_conversation_after_view_creation(
|
||||
RestoredAIConversation::new(child_conversation),
|
||||
true,
|
||||
RestoreConversationEntryBehavior::PreserveAgentViewState,
|
||||
ctx,
|
||||
);
|
||||
terminal_view.enter_agent_view(
|
||||
None,
|
||||
Some(child_conversation_id),
|
||||
AgentViewEntryOrigin::SharedSessionSelection,
|
||||
ctx,
|
||||
);
|
||||
// Shared-session viewer is `is_cloud_mode=false`, so
|
||||
// `ambient_agent_view_model()` is typically `None`. Update
|
||||
// opportunistically; the network's `JoinedSuccessfully` is the
|
||||
// authoritative source for ambient agent state.
|
||||
if let Some(ambient_agent_view_model) = terminal_view
|
||||
.ambient_agent_view_model()
|
||||
.into_optional_handle()
|
||||
.cloned()
|
||||
{
|
||||
ambient_agent_view_model.update(ctx, |model, ctx| {
|
||||
model.set_conversation_id(Some(child_conversation_id));
|
||||
if let Some(task_id) = child_task_id {
|
||||
model.enter_viewing_existing_session(task_id, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
self.child_agent_panes
|
||||
.insert(child_conversation_id, new_pane_id.into());
|
||||
// If the discarded fallback was occupying a tree slot via temporary
|
||||
// replacement, re-swap so the user lands on the new pane.
|
||||
if let Some(anchor) = fallback_was_swapped_anchor {
|
||||
self.swap_active_pane_to_conversation(anchor, child_conversation_id, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user