first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
@@ -1,9 +1,8 @@
use std::collections::HashMap;
use galaxy_core::features::FeatureFlag;
use galaxyui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity, WindowId};
use crate::settings::AISettings;
use galaxy_core::send_telemetry_from_ctx;
use galaxyui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity, ViewHandle, WindowId};
use crate::ai::active_agent_views_model::{ActiveAgentViewsEvent, ActiveAgentViewsModel};
use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
@@ -12,16 +11,16 @@ use crate::ai::agent_management::notifications::{
NotificationSourceAgent,
};
use crate::ai::artifacts::Artifact;
use crate::ai::blocklist::BlocklistAIHistoryEvent;
use crate::ai::blocklist::{BlocklistAIHistoryEvent, ConversationStatusUpdate, QueuedQueryModel};
use crate::server::telemetry::TelemetryEvent;
use crate::settings::AISettings;
use crate::terminal::cli_agent_sessions::{
CLIAgentSessionStatus, CLIAgentSessionsModel, CLIAgentSessionsModelEvent,
};
use crate::terminal::CLIAgent;
use crate::terminal::{CLIAgent, TerminalView};
use crate::workspace::util::is_terminal_view_in_same_tab;
use crate::workspace::{Workspace, WorkspaceRegistry};
use crate::BlocklistAIHistoryModel;
use galaxy_core::send_telemetry_from_ctx;
/// Singleton model responsible for triggering in-app notifications on blocking conversation
/// status updates and tracking/storing these notifications for the notifications mailbox.
@@ -43,17 +42,17 @@ impl SingletonEntity for AgentNotificationsModel {}
impl AgentNotificationsModel {
pub(crate) fn new(ctx: &mut ModelContext<Self>) -> Self {
let history_model = BlocklistAIHistoryModel::handle(ctx);
ctx.subscribe_to_model(&history_model, move |me, event, ctx| {
ctx.subscribe_to_model(&history_model, move |me, _, event, ctx| {
me.handle_history_event(event, ctx);
});
let cli_sessions_model = CLIAgentSessionsModel::handle(ctx);
ctx.subscribe_to_model(&cli_sessions_model, |me, event, ctx| {
ctx.subscribe_to_model(&cli_sessions_model, |me, _, event, ctx| {
me.handle_cli_agent_session_event(event, ctx);
});
let active_views_model = ActiveAgentViewsModel::handle(ctx);
ctx.subscribe_to_model(&active_views_model, |me, event, ctx| {
ctx.subscribe_to_model(&active_views_model, |me, _, event, ctx| {
me.handle_active_agent_views_changed(event, ctx);
});
@@ -165,14 +164,19 @@ impl AgentNotificationsModel {
CLIAgent::Codex => "Notification from Codex",
_ => "Task completed.",
};
let metadata = TerminalViewMetadata::lookup(*terminal_view_id, ctx);
self.add_notification(
title,
message.to_owned(),
NotificationCategory::Complete,
NotificationSourceAgent::CLI(*agent),
NotificationSourceAgent::CLI {
agent: *agent,
is_ambient: metadata.is_ambient,
},
NotificationOrigin::CLISession(*terminal_view_id),
*terminal_view_id,
vec![],
metadata.branch,
ctx,
);
}
@@ -180,16 +184,21 @@ impl AgentNotificationsModel {
let title = session_context
.display_title()
.unwrap_or_else(|| format!("{} needs attention", agent.display_name()));
let metadata = TerminalViewMetadata::lookup(*terminal_view_id, ctx);
self.add_notification(
title,
message
.clone()
.unwrap_or_else(|| "Waiting for input.".to_owned()),
NotificationCategory::Request,
NotificationSourceAgent::CLI(*agent),
NotificationSourceAgent::CLI {
agent: *agent,
is_ambient: metadata.is_ambient,
},
NotificationOrigin::CLISession(*terminal_view_id),
*terminal_view_id,
vec![],
metadata.branch,
ctx,
);
}
@@ -237,10 +246,11 @@ impl AgentNotificationsModel {
}
let BlocklistAIHistoryEvent::UpdatedConversationStatus {
terminal_view_id,
terminal_surface_id,
conversation_id,
// We shouldn't trigger toasts when restoring conversations on startup.
is_restored: false,
update: ConversationStatusUpdate::Changed { .. },
..
} = event
else {
return;
@@ -262,7 +272,7 @@ impl AgentNotificationsModel {
&status,
*conversation_id,
latest_query,
*terminal_view_id,
*terminal_surface_id,
ctx,
);
// The new mailbox path handled the event — skip the legacy toast path below.
@@ -273,7 +283,7 @@ impl AgentNotificationsModel {
return;
}
if is_terminal_view_visible(*terminal_view_id, ctx) {
if is_terminal_view_visible(*terminal_surface_id, ctx) {
return;
}
@@ -286,7 +296,7 @@ impl AgentNotificationsModel {
ctx.emit(AgentManagementEvent::ConversationNeedsAttention {
window_id,
tab_index,
terminal_view_id: *terminal_view_id,
terminal_view_id: *terminal_surface_id,
conversation_id: *conversation_id,
});
}
@@ -310,22 +320,36 @@ impl AgentNotificationsModel {
}
let title = latest_query.unwrap_or_else(|| "Agent task".to_owned());
let metadata = TerminalViewMetadata::lookup(terminal_view_id, ctx);
let oz_agent = NotificationSourceAgent::Oz {
is_ambient: metadata.is_ambient,
};
match status {
// When the agent resumes its work, clear stale notifications.
ConversationStatus::InProgress => {
// When the agent resumes its work (or is automatically recovering from a
// transient failure), clear stale notifications.
ConversationStatus::InProgress | ConversationStatus::TransientError => {
self.remove_notification_by_source(origin, ctx);
}
ConversationStatus::Success => {
// Suppress the completion notification when a queued follow-up prompt will
// auto-send as soon as this conversation finishes. The conversation isn't
// really in a stopped state, so the notification would be noisy. Pending
// artifacts are left intact so they roll into the notification fired when the
// conversation eventually finishes with an empty queue.
if QueuedQueryModel::as_ref(ctx).has_autofireable_prompt(conversation_id) {
return;
}
let artifacts = self.flush_pending_artifacts(conversation_id);
self.add_notification(
title,
"Task completed.".to_owned(),
NotificationCategory::Complete,
NotificationSourceAgent::Oz,
oz_agent,
origin,
terminal_view_id,
artifacts,
metadata.branch,
ctx,
);
}
@@ -335,10 +359,11 @@ impl AgentNotificationsModel {
title,
"Task was cancelled.".to_owned(),
NotificationCategory::Complete,
NotificationSourceAgent::Oz,
oz_agent,
origin,
terminal_view_id,
artifacts,
metadata.branch,
ctx,
);
}
@@ -347,10 +372,11 @@ impl AgentNotificationsModel {
title,
blocked_action.clone(),
NotificationCategory::Request,
NotificationSourceAgent::Oz,
oz_agent,
origin,
terminal_view_id,
vec![],
metadata.branch,
ctx,
);
}
@@ -360,13 +386,20 @@ impl AgentNotificationsModel {
title,
"Something went wrong.".to_owned(),
NotificationCategory::Error,
NotificationSourceAgent::Oz,
oz_agent,
origin,
terminal_view_id,
artifacts,
metadata.branch,
ctx,
);
}
// Yielded conversations are still active; mirror the
// InProgress arm and clear any stale notification for this
// origin.
ConversationStatus::WaitingForEvents => {
self.remove_notification_by_source(origin, ctx);
}
}
}
@@ -401,14 +434,12 @@ impl AgentNotificationsModel {
origin: NotificationOrigin,
terminal_view_id: EntityId,
artifacts: Vec<Artifact>,
branch: Option<String>,
ctx: &mut ModelContext<Self>,
) {
if !*AISettings::as_ref(ctx).show_agent_notifications {
return;
}
let show_agent_notifications = *AISettings::as_ref(ctx).show_agent_notifications;
let is_visible = is_terminal_view_visible(terminal_view_id, ctx);
let branch = resolve_git_branch_for_terminal_view(terminal_view_id, ctx);
let item = NotificationItem::new(
title,
message,
@@ -420,12 +451,14 @@ impl AgentNotificationsModel {
artifacts,
branch,
);
send_telemetry_from_ctx!(
TelemetryEvent::AgentNotificationShown {
agent_variant: agent.into(),
},
ctx
);
if show_agent_notifications {
send_telemetry_from_ctx!(
TelemetryEvent::AgentNotificationShown {
agent_variant: agent.into(),
},
ctx
);
}
let id = item.id;
self.notifications.push(item);
@@ -453,13 +486,22 @@ pub enum AgentManagementEvent {
impl ConversationStatus {
/// Returns true if the updating the conversation with this status should trigger some
/// notification to the user.
///
/// Exhaustive match so a new `ConversationStatus` variant forces a
/// deliberate decision about whether it should fire a notification.
pub fn should_trigger_notification(&self) -> bool {
matches!(
self,
match self {
ConversationStatus::Success
| ConversationStatus::Blocked { .. }
| ConversationStatus::Error
)
| ConversationStatus::Blocked { .. }
| ConversationStatus::Error => true,
// Streaming hasn't reached a notable state; a recovering or
// yielded conversation is still active; user-cancellations are
// self-evident.
ConversationStatus::InProgress
| ConversationStatus::TransientError
| ConversationStatus::WaitingForEvents
| ConversationStatus::Cancelled => false,
}
}
}
@@ -502,17 +544,41 @@ fn window_and_tab_idx_id_for_conversation(
})
}
fn resolve_git_branch_for_terminal_view(
/// Per-notification metadata derived from a single [`TerminalView`] lookup. Both fields
/// are read on the same emit path, so we resolve the view once and pass the projection
/// down rather than walking the workspace tree for each.
struct TerminalViewMetadata {
is_ambient: bool,
branch: Option<String>,
}
impl TerminalViewMetadata {
fn lookup(terminal_view_id: EntityId, app: &AppContext) -> Self {
let Some(terminal_view) = find_terminal_view_by_id(terminal_view_id, app) else {
return Self {
is_ambient: false,
branch: None,
};
};
let view = terminal_view.as_ref(app);
Self {
is_ambient: view.is_ambient_agent_session(app),
branch: view.current_git_branch(app),
}
}
}
fn find_terminal_view_by_id(
terminal_view_id: EntityId,
app: &AppContext,
) -> Option<String> {
) -> Option<ViewHandle<TerminalView>> {
for (_, workspace_handle) in WorkspaceRegistry::as_ref(app).all_workspaces(app) {
for pane_group in workspace_handle.as_ref(app).tab_views() {
let pane_group = pane_group.as_ref(app);
for pane_id in pane_group.terminal_pane_ids() {
if let Some(terminal_view) = pane_group.terminal_view_from_pane_id(pane_id, app) {
if terminal_view.id() == terminal_view_id {
return terminal_view.as_ref(app).current_git_branch(app);
return Some(terminal_view);
}
}
}
@@ -1,14 +1,19 @@
use settings::Setting as _;
use galaxy_core::features::FeatureFlag;
use galaxyui::{App, EntityId, ModelHandle};
use crate::ai::active_agent_views_model::ActiveAgentViewsModel;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::artifacts::Artifact;
use crate::ai::blocklist::BlocklistAIHistoryEvent;
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;
use crate::BlocklistAIHistoryModel;
use galaxyui::{App, EntityId, ModelHandle, SingletonEntity};
use super::AgentNotificationsModel;
use crate::ai::active_agent_views_model::ActiveAgentViewsModel;
use crate::ai::agent::conversation::{AIConversation, AIConversationId, ConversationStatus};
use crate::ai::agent_management::notifications::{
NotificationCategory, NotificationFilter, NotificationOrigin, NotificationSourceAgent,
};
use crate::ai::artifacts::Artifact;
use crate::ai::blocklist::BlocklistAIHistoryEvent;
use crate::settings::AISettings;
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::{report_if_error, BlocklistAIHistoryModel};
fn setup_app(
app: &mut App,
@@ -16,7 +21,11 @@ fn setup_app(
ModelHandle<BlocklistAIHistoryModel>,
ModelHandle<AgentNotificationsModel>,
) {
initialize_settings_for_tests(app);
let history = app.add_singleton_model(|_| BlocklistAIHistoryModel::new(vec![], &[]));
// Registered after the history model since it subscribes to history events; the
// notifications model reads it to suppress completion notifications when a prompt is queued.
app.add_singleton_model(crate::ai::blocklist::QueuedQueryModel::new);
app.add_singleton_model(|_| CLIAgentSessionsModel::new());
app.add_singleton_model(|_| ActiveAgentViewsModel::new());
let notifications = app.add_singleton_model(AgentNotificationsModel::new);
@@ -51,7 +60,7 @@ fn artifact_event_accumulates_into_pending() {
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::UpdatedConversationArtifacts {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id,
artifact: make_pr_artifact("https://github.com/org/repo/pull/42", "feature-branch"),
});
@@ -76,14 +85,14 @@ fn multiple_artifacts_accumulated_across_turns() {
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::UpdatedConversationArtifacts {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id,
artifact: make_plan_artifact("doc-1", "My Plan"),
});
});
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::UpdatedConversationArtifacts {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id,
artifact: make_pr_artifact("https://github.com/org/repo/pull/1", "main"),
});
@@ -98,6 +107,46 @@ fn multiple_artifacts_accumulated_across_turns() {
});
}
#[test]
fn add_notification_tracks_unread_activity_when_in_app_notifications_are_hidden() {
App::test((), |mut app| async move {
let _guard = FeatureFlag::HOANotifications.override_enabled(true);
let (_history, notifications) = setup_app(&mut app);
AISettings::handle(&app).update(&mut app, |settings, ctx| {
report_if_error!(settings.show_agent_notifications.set_value(false, ctx));
});
let conversation_id = AIConversationId::new();
let terminal_view_id = EntityId::new();
notifications.update(&mut app, |model, ctx| {
model.add_notification(
"Agent task".to_owned(),
"Task completed.".to_owned(),
NotificationCategory::Complete,
NotificationSourceAgent::Oz { is_ambient: false },
NotificationOrigin::Conversation(conversation_id),
terminal_view_id,
vec![],
None,
ctx,
);
});
notifications.read(&app, |model, _| {
assert_eq!(
model
.notifications()
.filtered_count(NotificationFilter::All),
1
);
assert!(model
.notifications()
.has_unread_for_terminal_view(terminal_view_id));
});
});
}
#[test]
fn flush_drains_pending_artifacts() {
App::test((), |mut app| async move {
@@ -109,7 +158,7 @@ fn flush_drains_pending_artifacts() {
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::UpdatedConversationArtifacts {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id,
artifact: make_pr_artifact("https://github.com/org/repo/pull/1", "branch-1"),
});
@@ -153,7 +202,7 @@ fn deletion_cleans_up_pending_artifacts() {
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::UpdatedConversationArtifacts {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id,
artifact: make_pr_artifact("https://github.com/org/repo/pull/1", "branch-1"),
});
@@ -161,9 +210,10 @@ fn deletion_cleans_up_pending_artifacts() {
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::DeletedConversation {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id,
conversation_title: None,
run_id: None,
});
});
@@ -185,14 +235,14 @@ fn separate_conversations_have_independent_pending_artifacts() {
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::UpdatedConversationArtifacts {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id: conv_a,
artifact: make_pr_artifact("https://github.com/org/repo/pull/1", "branch-a"),
});
});
history.update(&mut app, |_: &mut BlocklistAIHistoryModel, ctx| {
ctx.emit(BlocklistAIHistoryEvent::UpdatedConversationArtifacts {
terminal_view_id,
terminal_surface_id: terminal_view_id,
conversation_id: conv_b,
artifact: make_plan_artifact("doc-b", "Plan B"),
});
@@ -209,3 +259,178 @@ fn separate_conversations_have_independent_pending_artifacts() {
});
});
}
// should_trigger_notification: pure-function tests pinning which statuses
// fire user-facing notifications. Terminal-error and blocked surface;
// in-progress, waiting-for-events, and user-cancelled do not.
#[test]
fn should_trigger_notification_returns_true_for_success() {
assert!(ConversationStatus::Success.should_trigger_notification());
}
#[test]
fn should_trigger_notification_returns_true_for_blocked() {
assert!(ConversationStatus::Blocked {
blocked_action: "approve diff".to_owned(),
}
.should_trigger_notification());
}
#[test]
fn should_trigger_notification_returns_true_for_error() {
assert!(ConversationStatus::Error.should_trigger_notification());
}
#[test]
fn should_trigger_notification_returns_false_for_in_progress() {
assert!(!ConversationStatus::InProgress.should_trigger_notification());
}
#[test]
fn should_trigger_notification_returns_false_for_waiting_for_events() {
assert!(!ConversationStatus::WaitingForEvents.should_trigger_notification());
}
#[test]
fn should_trigger_notification_returns_false_for_cancelled() {
assert!(!ConversationStatus::Cancelled.should_trigger_notification());
}
// Mailbox suppression for non-terminal status updates. In App::test the
// `is_conversation_open` gate always returns false, so the
// WaitingForEvents and InProgress arms both clear stale notifications
// regardless of status; this still pins the user-visible contract that
// no stale "Task completed" toast survives a non-terminal transition.
/// Disables `show_agent_notifications` so subsequent `add_notification`
/// calls skip the `send_telemetry_from_ctx!` branch — the test app does
/// not register a `TelemetryContextProvider` singleton and the macro
/// would otherwise panic.
fn disable_telemetry_path(app: &mut App) {
AISettings::handle(app).update(app, |settings, ctx| {
report_if_error!(settings.show_agent_notifications.set_value(false, ctx));
});
}
/// Pre-populates a `Complete` notification for `conversation_id` so that a
/// subsequent non-terminal status update has something to clear.
fn seed_stale_notification(
notifications: &ModelHandle<AgentNotificationsModel>,
app: &mut App,
conversation_id: AIConversationId,
terminal_view_id: EntityId,
) {
notifications.update(app, |model, ctx| {
model.add_notification(
"Agent task".to_owned(),
"Task completed.".to_owned(),
NotificationCategory::Complete,
NotificationSourceAgent::Oz { is_ambient: false },
NotificationOrigin::Conversation(conversation_id),
terminal_view_id,
vec![],
None,
ctx,
);
});
}
#[test]
fn waiting_for_events_clears_stale_notification_and_adds_none() {
App::test((), |mut app| async move {
let _guard = FeatureFlag::HOANotifications.override_enabled(true);
let (history, notifications) = setup_app(&mut app);
disable_telemetry_path(&mut app);
let conversation = AIConversation::new(false, false);
let conversation_id = conversation.id();
let terminal_view_id = EntityId::new();
history.update(&mut app, |model, ctx| {
model.restore_conversations(terminal_view_id, vec![conversation], ctx);
});
seed_stale_notification(&notifications, &mut app, conversation_id, terminal_view_id);
notifications.read(&app, |model, _| {
assert_eq!(
model
.notifications()
.filtered_count(NotificationFilter::All),
1,
"precondition: one stale notification queued"
);
});
history.update(&mut app, |model, ctx| {
let conv = model
.conversation_mut(&conversation_id)
.expect("conversation was just restored");
conv.update_status(ConversationStatus::WaitingForEvents, terminal_view_id, ctx);
});
notifications.read(&app, |model, _| {
assert_eq!(
model
.notifications()
.filtered_count(NotificationFilter::All),
0,
"WaitingForEvents must clear stale notifications and add no new toast"
);
});
});
}
#[test]
fn in_progress_resume_clears_stale_notification_and_adds_none() {
App::test((), |mut app| async move {
let _guard = FeatureFlag::HOANotifications.override_enabled(true);
let (history, notifications) = setup_app(&mut app);
disable_telemetry_path(&mut app);
let conversation = AIConversation::new(false, false);
let conversation_id = conversation.id();
let terminal_view_id = EntityId::new();
history.update(&mut app, |model, ctx| {
model.restore_conversations(terminal_view_id, vec![conversation], ctx);
});
// First move the conversation into WaitingForEvents, then back into
// InProgress. The second transition is the resume signal that
// PRODUCT.md (18) requires not to fire a notification.
history.update(&mut app, |model, ctx| {
let conv = model
.conversation_mut(&conversation_id)
.expect("conversation was just restored");
conv.update_status(ConversationStatus::WaitingForEvents, terminal_view_id, ctx);
});
seed_stale_notification(&notifications, &mut app, conversation_id, terminal_view_id);
notifications.read(&app, |model, _| {
assert_eq!(
model
.notifications()
.filtered_count(NotificationFilter::All),
1,
"precondition: one stale notification queued before the resume transition"
);
});
history.update(&mut app, |model, ctx| {
let conv = model
.conversation_mut(&conversation_id)
.expect("conversation still exists");
conv.update_status(ConversationStatus::InProgress, terminal_view_id, ctx);
});
notifications.read(&app, |model, _| {
assert_eq!(
model
.notifications()
.filtered_count(NotificationFilter::All),
0,
"WaitingForEvents → InProgress resume must not fire a notification \
(covers PRODUCT.md (18))"
);
});
});
}
@@ -3,8 +3,8 @@
//! This modal is displayed when users click "New agent" to choose between
//! cloud and local agent modes.
use crate::appearance::Appearance;
use crate::ui_components::icons::Icon;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use galaxy_core::ui::color::blend::Blend;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::{
@@ -17,8 +17,9 @@ use galaxyui::keymap::{FixedBinding, Keystroke};
use galaxyui::platform::Cursor;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use crate::appearance::Appearance;
use crate::ui_components::icons::Icon;
// Modal dimensions based on Figma design.
const MODAL_WIDTH: f32 = 440.;
@@ -1,3 +1,25 @@
use std::collections::HashMap;
use serde::Serialize;
use string_offset::CharCounter;
use warp_completer::signatures::CommandRegistry;
use warp_completer::util::parse_current_commands_and_tokens;
use warp_completer::ParsedTokensSnapshot;
use galaxy_core::report_error;
use galaxy_core::ui::theme::{AnsiColorIdentifier, AnsiColors};
use warpui::clipboard::ClipboardContent;
use warpui::elements::new_scrollable::{ClippedAxisConfiguration, DualAxisConfig, NewScrollable};
use warpui::elements::{
Align, Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, Element, Empty, Expanded, Flex, Highlight, HighlightedRange,
MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::prelude::ChildView;
use warpui::text_layout::TextStyle;
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
use warpui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle};
use crate::ai::agent_management::telemetry::{AgentManagementTelemetryEvent, SetupGuideStep};
use crate::ai::blocklist::code_block::{
render_code_block_plain, CodeBlockOptions, CodeSnippetButtonHandles,
@@ -8,26 +30,6 @@ use crate::send_telemetry_from_ctx;
use crate::view_components::action_button::{ActionButton, SecondaryTheme};
use crate::workflows::workflow::{Argument, ArgumentType, Workflow};
use crate::workflows::WorkflowType;
use galaxy_completer::signatures::CommandRegistry;
use galaxy_completer::{util::parse_current_commands_and_tokens, ParsedTokensSnapshot};
use galaxy_core::report_error;
use galaxy_core::ui::theme::{AnsiColorIdentifier, AnsiColors};
use galaxyui::clipboard::ClipboardContent;
use galaxyui::elements::{
new_scrollable::{ClippedAxisConfiguration, DualAxisConfig, NewScrollable},
Align, Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, Element, Empty, Expanded, Flex, Highlight, HighlightedRange,
MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::prelude::ChildView;
use galaxyui::text_layout::TextStyle;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::ViewHandle;
use galaxyui::{AppContext, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use serde::Serialize;
use std::collections::HashMap;
use string_offset::CharCounter;
const DOCS_URL: &str = "https://docs.warp.dev/agent-platform/cloud-agents/overview";
const ENV_DOCS_URL: &str =
@@ -412,6 +414,7 @@ impl CloudSetupGuideView {
mouse_handles: Some(handles),
file_path: None,
},
true,
app,
None,
)
@@ -4,14 +4,12 @@ use galaxy_core::ui::theme::AnsiColorIdentifier;
use galaxyui::elements::{ChildView, CrossAxisAlignment, Empty, Flex, ParentElement};
use galaxyui::{AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle};
use crate::view_components::copyable_text_field::COPY_FEEDBACK_DURATION;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent_conversations_model::AgentRunDisplayStatus;
use crate::ai::agent_management::view::ManagementCardItemId;
use crate::ai::agent_conversations_model::{AgentConversationEntryId, AgentRunDisplayStatus};
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{ActionButton, ButtonSize, SecondaryTheme};
use crate::view_components::copyable_text_field::COPY_FEEDBACK_DURATION;
use crate::workspace::WorkspaceAction;
const BUTTON_SPACING: f32 = 4.;
@@ -25,7 +23,7 @@ pub struct ActionButtonsConfig {
pub fork_conversation_id: Option<AIConversationId>,
/// Shows an info button for viewing more details.
/// Only used in management view hover toolbelt.
pub view_details_item_id: Option<ManagementCardItemId>,
pub view_details_item_id: Option<AgentConversationEntryId>,
/// Conversation link URL (either to the transcript or live session) for copy link button.
pub copy_link_url: Option<String>,
}
@@ -87,7 +85,7 @@ pub enum AgentDetailsButtonEvent {
Open,
CancelTask { task_id: AmbientAgentTaskId },
ForkConversation { conversation_id: AIConversationId },
ViewDetails { item_id: ManagementCardItemId },
ViewDetails { item_id: AgentConversationEntryId },
CopyLink { link: String },
}
@@ -260,9 +258,7 @@ impl TypedActionView for ConversationActionButtonsRow {
}
AgentDetailsAction::ViewDetails => {
if let Some(item_id) = &self.config.view_details_item_id {
ctx.emit(AgentDetailsButtonEvent::ViewDetails {
item_id: item_id.clone(),
});
ctx.emit(AgentDetailsButtonEvent::ViewDetails { item_id: *item_id });
}
}
AgentDetailsAction::CopyLink => {
@@ -42,12 +42,23 @@ impl NotificationFilter {
}
}
/// Identifies the agent that produced a notification.
/// Identifies the agent that produced a notification, including whether the run was
/// ambient (cloud) or local. The `is_ambient` flag drives the cloud-lobe rendering in
/// [`render_agent_avatar`].
#[derive(Debug, Clone, Copy)]
#[allow(clippy::upper_case_acronyms)]
pub enum NotificationSourceAgent {
Oz,
CLI(CLIAgent),
Oz { is_ambient: bool },
CLI { agent: CLIAgent, is_ambient: bool },
}
impl NotificationSourceAgent {
pub fn is_ambient(&self) -> bool {
match self {
NotificationSourceAgent::Oz { is_ambient }
| NotificationSourceAgent::CLI { is_ambient, .. } => *is_ambient,
}
}
}
/// Identifies the conversation or session a notification belongs to.
@@ -1,7 +1,10 @@
use std::sync::Arc;
use pathfinder_color::ColorU;
use galaxy_core::ui::appearance::Appearance as CoreAppearance;
use galaxy_core::ui::icons::Icon;
use galaxy_core::ui::theme::{Fill, GalaxyTheme};
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::{Fill, WarpTheme};
use galaxyui::clipboard::ClipboardContent;
use galaxyui::elements::{
ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DispatchEventResult,
@@ -11,10 +14,6 @@ use galaxyui::elements::{
use galaxyui::fonts::Weight;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{View, ViewContext, ViewHandle};
use pathfinder_color::ColorU;
use galaxy_core::ui::appearance::Appearance as CoreAppearance;
use galaxy_core::ui::theme::color::internal_colors;
use crate::ai::agent::conversation::ConversationStatus;
use crate::ai::agent_management::notifications::item::NotificationSourceAgent;
@@ -25,9 +24,7 @@ use crate::ai::artifacts::{
};
use crate::appearance::Appearance;
use crate::send_telemetry_from_ctx;
use crate::ui_components::icon_with_status::{
render_icon_with_status, IconWithStatusSizing, IconWithStatusVariant,
};
use crate::ui_components::icon_with_status::{render_icon_with_status, IconWithStatusVariant};
use crate::util::time_format::format_elapsed_since;
use crate::view_components::action_button::ActionButtonTheme;
use crate::workspace::WorkspaceAction;
@@ -399,14 +396,8 @@ fn render_message_text(message: &str, expanded: bool, appearance: &Appearance) -
.finish()
}
const NOTIFICATION_AVATAR_SIZING: IconWithStatusSizing = IconWithStatusSizing {
icon_size: 16.,
padding: 8.,
badge_icon_size: 12.,
badge_padding: 2.,
overall_size_override: None,
badge_offset: (6., 6.),
};
/// Total size of the agent avatar component rendered alongside each notification.
const NOTIFICATION_AVATAR_SIZE: f32 = 32.;
fn render_agent_avatar(
agent: NotificationSourceAgent,
@@ -415,18 +406,20 @@ fn render_agent_avatar(
) -> Box<dyn Element> {
let status = notification_category_to_conversation_status(category);
let variant = match agent {
NotificationSourceAgent::Oz => IconWithStatusVariant::OzAgent {
NotificationSourceAgent::Oz { is_ambient } => IconWithStatusVariant::OzAgent {
status: Some(status),
is_ambient: false,
is_ambient,
},
NotificationSourceAgent::CLI(cli) => IconWithStatusVariant::CLIAgent {
agent: cli,
NotificationSourceAgent::CLI { agent, is_ambient } => IconWithStatusVariant::CLIAgent {
agent,
status: Some(status),
is_ambient,
},
};
render_icon_with_status(
variant,
&NOTIFICATION_AVATAR_SIZING,
NOTIFICATION_AVATAR_SIZE,
0.,
theme,
theme.surface_2(),
)
@@ -12,7 +12,7 @@ fn make_conversation_notification(
"test".to_owned(),
"msg".to_owned(),
NotificationCategory::Complete,
NotificationSourceAgent::Oz,
NotificationSourceAgent::Oz { is_ambient: false },
NotificationOrigin::Conversation(conversation_id),
false,
terminal_view_id,
@@ -26,7 +26,10 @@ fn make_cli_session_notification(terminal_view_id: EntityId) -> NotificationItem
"cli test".to_owned(),
"cli msg".to_owned(),
NotificationCategory::Complete,
NotificationSourceAgent::CLI(CLIAgent::Claude),
NotificationSourceAgent::CLI {
agent: CLIAgent::Claude,
is_ambient: false,
},
NotificationOrigin::CLISession(terminal_view_id),
false,
terminal_view_id,
+20
View File
@@ -108,9 +108,14 @@ pub enum AgentManagementTelemetryEvent {
/// User clicked "Continue locally" in the tombstone
#[cfg(not(target_family = "wasm"))]
TombstoneContinueLocally,
/// User clicked "Continue" in the tombstone to start a cloud follow-up.
TombstoneContinueInCloud { task_id: String },
/// User clicked "Continue locally" in the details panel
#[cfg(not(target_family = "wasm"))]
DetailsPanelContinueLocally,
/// User invoked the /continue-locally slash command
#[cfg(not(target_family = "wasm"))]
SlashCommandContinueLocally,
/// User clicked "Open in Warp" in the tombstone (wasm)
#[cfg(target_family = "wasm")]
TombstoneOpenInWarp,
@@ -190,8 +195,13 @@ impl TelemetryEvent for AgentManagementTelemetryEvent {
}
#[cfg(not(target_family = "wasm"))]
AgentManagementTelemetryEvent::TombstoneContinueLocally => None,
AgentManagementTelemetryEvent::TombstoneContinueInCloud { task_id } => Some(json!({
"task_id": task_id,
})),
#[cfg(not(target_family = "wasm"))]
AgentManagementTelemetryEvent::DetailsPanelContinueLocally => None,
#[cfg(not(target_family = "wasm"))]
AgentManagementTelemetryEvent::SlashCommandContinueLocally => None,
#[cfg(target_family = "wasm")]
AgentManagementTelemetryEvent::TombstoneOpenInWarp => None,
AgentManagementTelemetryEvent::CloudRunCancelled { task_id } => {
@@ -242,8 +252,11 @@ impl TelemetryEventDesc for AgentManagementTelemetryEventDiscriminants {
Self::TombstoneArtifactClicked => "AgentManagement.TombstoneArtifactClicked",
#[cfg(not(target_family = "wasm"))]
Self::TombstoneContinueLocally => "AgentManagement.TombstoneContinueLocally",
Self::TombstoneContinueInCloud => "AgentManagement.TombstoneContinueInCloud",
#[cfg(not(target_family = "wasm"))]
Self::DetailsPanelContinueLocally => "AgentManagement.DetailsPanelContinueLocally",
#[cfg(not(target_family = "wasm"))]
Self::SlashCommandContinueLocally => "AgentManagement.SlashCommandContinueLocally",
#[cfg(target_family = "wasm")]
Self::TombstoneOpenInWarp => "AgentManagement.TombstoneOpenInWarp",
Self::CloudRunCancelled => "AgentManagement.CloudRunCancelled",
@@ -274,10 +287,17 @@ impl TelemetryEventDesc for AgentManagementTelemetryEventDiscriminants {
Self::TombstoneArtifactClicked => "User clicked an artifact in the tombstone view",
#[cfg(not(target_family = "wasm"))]
Self::TombstoneContinueLocally => "User clicked Continue locally in the tombstone",
Self::TombstoneContinueInCloud => {
"User clicked Continue in the tombstone to start a cloud follow-up"
}
#[cfg(not(target_family = "wasm"))]
Self::DetailsPanelContinueLocally => {
"User clicked Continue locally in the details panel"
}
#[cfg(not(target_family = "wasm"))]
Self::SlashCommandContinueLocally => {
"User invoked /continue-locally to fork a cloud conversation locally"
}
#[cfg(target_family = "wasm")]
Self::TombstoneOpenInWarp => "User clicked Open in Warp in the tombstone",
Self::CloudRunCancelled => "User cancelled a cloud run",
+305 -308
View File
@@ -8,62 +8,9 @@ use galaxyui::scene::DropShadow;
use galaxyui::ui_components::button::ButtonVariant;
use lazy_static::lazy_static;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use settings::Setting;
use siphasher::sip::SipHasher;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent_conversations_model::{
AgentConversationsModel, AgentConversationsModelEvent, AgentManagementFilters, ArtifactFilter,
ConversationOrTask, CreatedOnFilter, CreatorFilter, EnvironmentFilter, HarnessFilter,
OwnerFilter, SessionStatus, SourceFilter, StatusFilter,
};
use crate::ai::agent_management::agent_type_selector::{
AgentType, AgentTypeSelector, AgentTypeSelectorEvent,
};
use crate::ai::agent_management::cloud_setup_guide_view::{
CloudSetupGuideEvent, CloudSetupGuideView,
};
use crate::ai::agent_management::details_action_buttons::{
ActionButtonsConfig, AgentDetailsButtonEvent, ConversationActionButtonsRow,
};
use crate::ai::agent_management::telemetry::{
AgentManagementTelemetryEvent, ArtifactType, FilterType, OpenedFrom,
};
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::ambient_agents::{cancel_task_with_toast, AgentSource};
use crate::ai::artifacts::{Artifact, ArtifactButtonsRow, ArtifactButtonsRowEvent};
use crate::ai::blocklist::history_model::BlocklistAIHistoryModel;
use crate::ai::conversation_details_panel::{
ConversationDetailsData, ConversationDetailsPanel, ConversationDetailsPanelEvent,
};
use crate::ai::conversation_status_ui::render_status_element;
use crate::ai::harness_display;
use crate::app_state::PersistedAgentManagementFilters;
use crate::appearance::Appearance;
use crate::auth::AuthStateProvider;
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys,
PropagateHorizontalNavigationKeys, SingleLineEditorOptions, TextOptions,
};
use crate::menu::{MenuItem, MenuItemFields};
use crate::notebooks::NotebookId;
use crate::settings::ai::AISettings;
use crate::ui_components::avatar::{Avatar, AvatarContent};
use crate::util::time_format::format_approx_duration_from_now_utc;
use crate::view_components::action_button::{
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
};
use crate::view_components::compactible_action_button::{
CompactibleActionButton, MEDIUM_SIZE_SWITCH_THRESHOLD,
};
use crate::view_components::dropdown::{Dropdown, DropdownAction, DropdownStyle};
use crate::view_components::DismissibleToast;
use crate::view_components::FilterableDropdown;
use crate::workflows::WorkflowType;
use crate::workspace::{ForkedConversationDestination, ToastStack};
use crate::workspace::{RestoreConversationLayout, WorkspaceAction};
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::{send_telemetry_from_ctx, AgentModeEntrypoint};
use galaxy_cli::agent::Harness;
use galaxy_core::ui::icons::Icon;
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::Fill;
@@ -79,15 +26,71 @@ use galaxyui::elements::{
SizeConstraintCondition, SizeConstraintSwitch, Stack, Text, Wrap,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::keymap::FixedBinding;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::ui_components::components::UiComponentStyles;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{
keymap::FixedBinding, Action, AppContext, Entity, FocusContext, ModelHandle, SingletonEntity,
TypedActionView, View, ViewContext, ViewHandle, WeakViewHandle,
AppContext, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use pathfinder_geometry::vector::vec2f;
use settings::Setting;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent_conversations_model::{
AgentConversationEntry, AgentConversationEntryId, AgentConversationNavigationSubject,
AgentConversationsModel, AgentConversationsModelEvent, AgentManagementFilters, ArtifactFilter,
ConversationUpdateKind, CreatedOnFilter, CreatorFilter, EnvironmentFilter, HarnessFilter,
OwnerFilter, SessionStatus, SourceFilter, StatusFilter,
};
use crate::ai::agent_management::agent_type_selector::{
AgentType, AgentTypeSelector, AgentTypeSelectorEvent,
};
use crate::ai::agent_management::cloud_setup_guide_view::{
CloudSetupGuideEvent, CloudSetupGuideView,
};
use crate::ai::agent_management::details_action_buttons::{
ActionButtonsConfig, AgentDetailsButtonEvent, ConversationActionButtonsRow,
};
use crate::ai::agent_management::telemetry::{
AgentManagementTelemetryEvent, ArtifactType, FilterType, OpenedFrom,
};
use crate::ai::ambient_agents::{cancel_task_with_toast, AgentSource};
use crate::ai::artifacts::{Artifact, ArtifactButtonsRow, ArtifactButtonsRowEvent};
use crate::ai::blocklist::format_credits;
use crate::ai::conversation_details_panel::{
ConversationDetailsData, ConversationDetailsPanel, ConversationDetailsPanelEvent,
};
use crate::ai::harness_availability::HarnessAvailabilityModel;
use crate::ai::harness_display;
use crate::app_state::PersistedAgentManagementFilters;
use crate::appearance::Appearance;
use crate::auth::AuthStateProvider;
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys,
PropagateHorizontalNavigationKeys, SingleLineEditorOptions, TextOptions,
};
use crate::menu::{MenuItem, MenuItemFields};
use crate::notebooks::NotebookId;
use crate::settings::ai::AISettings;
use crate::ui_components::agent_icon::agent_conversation_entry_icon_variant;
use crate::ui_components::avatar::{Avatar, AvatarContent};
use crate::ui_components::icon_with_status::render_icon_with_status;
use crate::util::time_format::format_approx_duration_from_now_utc;
use crate::view_components::action_button::{
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
};
use crate::view_components::compactible_action_button::{
CompactibleActionButton, MEDIUM_SIZE_SWITCH_THRESHOLD,
};
use crate::view_components::dropdown::{
Dropdown, DropdownAction, DropdownItemAction, DropdownStyle,
};
use crate::view_components::{DismissibleToast, FilterableDropdown};
use crate::workflows::WorkflowType;
use crate::workspace::{
ForkedConversationDestination, RestoreConversationLayout, ToastStack, WorkspaceAction,
};
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::{send_telemetry_from_ctx, AgentModeEntrypoint};
lazy_static! {
static ref HASHER: SipHasher = SipHasher::new_with_keys(0, 0);
@@ -105,8 +108,9 @@ const CARD_CONTENT_PADDING: f32 = 12.;
const CARD_BORDER_RADIUS: f32 = 4.;
const CARD_MARGIN_BOTTOM: f32 = 8.;
const STATUS_ICON_SIZE: f32 = 12.;
const BUTTON_SIZE: f32 = 20.;
/// Total size of the agent icon-with-status component rendered in each card's header row.
const CARD_AGENT_ICON_SIZE: f32 = 24.;
const CREATOR_AVATAR_FONT_SIZE: f32 = 10.;
const SESSION_EXPIRED_TEXT: &str = "Sessions expire after one week and cannot be opened.";
@@ -125,21 +129,7 @@ fn should_show_artifacts(artifacts: &[Artifact]) -> bool {
!artifacts.is_empty() && FeatureFlag::ConversationArtifacts.is_enabled()
}
/// Identifies a card item - either a task ID or a conversation ID
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ManagementCardItemId {
Task(AmbientAgentTaskId),
Conversation(AIConversationId),
}
impl ManagementCardItemId {
fn as_key(&self) -> String {
match self {
ManagementCardItemId::Task(id) => format!("task_{id}"),
ManagementCardItemId::Conversation(id) => format!("conv_{id}"),
}
}
}
pub type ManagementCardItemId = AgentConversationEntryId;
/// Store state for a given task row
struct CardState {
@@ -222,6 +212,13 @@ impl AgentManagementView {
Self::handle_agent_management_model_event,
);
ctx.subscribe_to_model(
&HarnessAvailabilityModel::handle(ctx),
|me, _, _event, ctx| {
me.update_harness_dropdown(ctx);
},
);
let list_state = Self::construct_fresh_list_state(ctx.handle());
let all_filter_button = ctx.add_typed_action_view(|_ctx| {
@@ -496,7 +493,7 @@ impl AgentManagementView {
let make_status_option =
|label: &str, action: AgentManagementViewAction, icon_data: Option<(Icon, Fill)>| {
let mut fields = MenuItemFields::new(label)
.with_on_select_action(DropdownAction::SelectActionAndClose(action));
.with_on_select_action(DropdownAction::select_action_and_close(action));
if let Some((icon, color)) = icon_data {
fields = fields.with_icon(icon).with_override_icon_color(color);
}
@@ -546,7 +543,7 @@ impl AgentManagementView {
}
/// Build the list of source filter items.
fn build_source_dropdown_items() -> Vec<MenuItem<DropdownAction<AgentManagementViewAction>>> {
fn build_source_dropdown_items() -> Vec<MenuItem<DropdownAction>> {
// Build up the sources list
let mut sources = vec![
AgentSource::WebApp,
@@ -564,14 +561,16 @@ impl AgentManagementView {
}
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(DropdownAction::SelectActionAndClose(
AgentManagementViewAction::SetSourceFilter(SourceFilter::All),
)),
MenuItemFields::new("All").with_on_select_action(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetSourceFilter(SourceFilter::All),
),
),
)];
for source in sources {
items.push(MenuItem::Item(
MenuItemFields::new(source.display_name()).with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetSourceFilter(SourceFilter::Specific(source)),
),
),
@@ -601,22 +600,22 @@ impl AgentManagementView {
let items = vec![
MenuItem::Item(MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::All),
),
)),
MenuItem::Item(MenuItemFields::new("Last 24 hours").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Last24Hours),
),
)),
MenuItem::Item(MenuItemFields::new("Past 3 days").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Past3Days),
),
)),
MenuItem::Item(MenuItemFields::new("Last week").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::LastWeek),
),
)),
@@ -635,29 +634,29 @@ impl AgentManagementView {
let items = vec![
MenuItem::Item(MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::All,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::All),
),
)),
MenuItem::Item(MenuItemFields::new("Pull Request").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::PullRequest,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::PullRequest),
),
)),
MenuItem::Item(MenuItemFields::new("Plan").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::Plan,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Plan),
),
)),
MenuItem::Item(MenuItemFields::new("Screenshot").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::Screenshot,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Screenshot),
),
)),
MenuItem::Item(MenuItemFields::new("File").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::File,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::File),
),
)),
];
@@ -672,17 +671,27 @@ impl AgentManagementView {
let mut dropdown = Dropdown::new(ctx);
Self::setup_filter_menu(&mut dropdown, "Harness", ctx);
// "All" has no leading icon, matching the Status dropdown's "All" row.
let items = Self::build_harness_dropdown_items(ctx);
dropdown.set_rich_items(items, ctx);
dropdown.set_selected_by_index(0, ctx);
dropdown
}
fn build_harness_dropdown_items(app: &AppContext) -> Vec<MenuItem<DropdownAction>> {
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(DropdownAction::SelectActionAndClose(
AgentManagementViewAction::SetHarnessFilter(HarnessFilter::All),
)),
MenuItemFields::new("All").with_on_select_action(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetHarnessFilter(HarnessFilter::All),
),
),
)];
for harness in [Harness::Oz, Harness::Claude, Harness::Gemini] {
let mut fields = MenuItemFields::new(harness_display::display_name(harness))
let availability = HarnessAvailabilityModel::as_ref(app);
for entry in availability.available_harnesses() {
let harness = entry.harness;
let mut fields = MenuItemFields::new(entry.display_name.clone())
.with_icon(harness_display::icon_for(harness))
.with_on_select_action(DropdownAction::SelectActionAndClose(
.with_on_select_action(DropdownAction::select_action_and_close(
AgentManagementViewAction::SetHarnessFilter(HarnessFilter::Specific(harness)),
));
if let Some(color) = harness_display::brand_color(harness) {
@@ -691,9 +700,7 @@ impl AgentManagementView {
items.push(MenuItem::Item(fields));
}
dropdown.set_rich_items(items, ctx);
dropdown.set_selected_by_index(0, ctx);
dropdown
items
}
fn create_environment_dropdown(
@@ -732,7 +739,7 @@ impl AgentManagementView {
}
// Initialize the dropdown menu for the filter dropdowns (status, source)
fn setup_filter_menu<A: Action + Clone>(
fn setup_filter_menu<A: DropdownItemAction>(
dropdown: &mut Dropdown<A>,
label_prefix: &'static str,
ctx: &mut ViewContext<Dropdown<A>>,
@@ -744,7 +751,7 @@ impl AgentManagementView {
}
// Initialize the dropdown menu for the searchable filter dropdowns (creator)
fn setup_searchable_filter_menu<A: Action + Clone>(
fn setup_searchable_filter_menu<A: DropdownItemAction>(
dropdown: &mut FilterableDropdown<A>,
label_prefix: &'static str,
ctx: &mut ViewContext<FilterableDropdown<A>>,
@@ -755,6 +762,13 @@ impl AgentManagementView {
dropdown.set_button_variant(ButtonVariant::Secondary);
}
fn update_harness_dropdown(&mut self, ctx: &mut ViewContext<Self>) {
let items = Self::build_harness_dropdown_items(ctx);
self.harness_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_rich_items(items, ctx);
});
}
/// Since the valid set of environments depends on what tasks we have loaded in,
/// we use this function to update the available options depending on the most recent
/// set of tasks.
@@ -771,7 +785,7 @@ impl AgentManagementView {
self.environment_dropdown.update(ctx, |dropdown, ctx| {
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetEnvironmentFilter(EnvironmentFilter::All),
),
),
@@ -779,7 +793,7 @@ impl AgentManagementView {
items.push(MenuItem::Item(
MenuItemFields::new("None").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetEnvironmentFilter(
EnvironmentFilter::NoEnvironment,
),
@@ -793,7 +807,7 @@ impl AgentManagementView {
for (environment_id, environment_name) in sorted_envs {
items.push(MenuItem::Item(
MenuItemFields::new(environment_name).with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetEnvironmentFilter(
EnvironmentFilter::Specific(environment_id),
),
@@ -818,7 +832,7 @@ impl AgentManagementView {
self.creator_dropdown.update(ctx, |dropdown, ctx| {
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatorFilter(CreatorFilter::All),
),
),
@@ -826,7 +840,7 @@ impl AgentManagementView {
for (name, uid) in creators {
items.push(MenuItem::Item(
MenuItemFields::new(&name).with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatorFilter(CreatorFilter::Specific {
name,
uid,
@@ -938,46 +952,34 @@ impl AgentManagementView {
let model = AgentConversationsModel::as_ref(ctx);
let search_query = self.search_query.trim().to_lowercase();
let cards: Vec<CardData> = model
.get_tasks_and_conversations(&self.filters, ctx)
.filter(|t| {
.get_entries(&self.filters, ctx)
.into_iter()
.filter(|entry| {
if search_query.is_empty() {
return true;
}
match_indices_case_insensitive(&t.title(ctx), &search_query).is_some()
match_indices_case_insensitive(&entry.display.title, &search_query).is_some()
})
.map(|t| {
let item_id = match t {
ConversationOrTask::Task(task) => ManagementCardItemId::Task(task.task_id),
ConversationOrTask::Conversation(conversation) => {
ManagementCardItemId::Conversation(conversation.nav_data.id)
}
};
let artifacts = t.artifacts(ctx);
let copy_link_url = t.session_or_conversation_link(ctx);
let mut config = match t {
ConversationOrTask::Task(task) => ActionButtonsConfig::for_task(
task.task_id,
&t.display_status(ctx),
None, // Don't show open button in card hover
copy_link_url,
),
ConversationOrTask::Conversation(conversation) => {
ActionButtonsConfig::for_conversation(
conversation.nav_data.id,
None, // Don't show open button in card hover
copy_link_url,
.map(|entry| {
let item_id = entry.id;
let copy_link_url = entry
.capabilities
.can_copy_link
.then(|| {
AgentConversationsModel::resolve_copy_link(
AgentConversationNavigationSubject::Entry(entry.id),
ctx,
)
}
};
// Show info button in card hover for ViewDetails if feature flag enabled
})
.flatten();
let mut config = Self::action_buttons_config_for_entry(&entry, None, copy_link_url);
if FeatureFlag::AgentManagementDetailsView.is_enabled() {
config.view_details_item_id = Some(item_id.clone());
config.view_details_item_id = Some(item_id);
}
CardData {
item_id,
artifacts,
artifacts: entry.display.artifacts,
action_buttons_config: config,
}
})
@@ -1026,11 +1028,8 @@ impl AgentManagementView {
} else {
None
};
let action_buttons_view = self.create_action_buttons_view(
card.item_id.clone(),
card.action_buttons_config,
ctx,
);
let action_buttons_view =
self.create_action_buttons_view(card.item_id, card.action_buttons_config, ctx);
new_items.push(CardState {
hover_state: MouseStateHandle::default(),
@@ -1074,6 +1073,29 @@ impl AgentManagementView {
view
}
fn action_buttons_config_for_entry(
entry: &AgentConversationEntry,
open_action: Option<WorkspaceAction>,
copy_link_url: Option<String>,
) -> ActionButtonsConfig {
if let Some(task_id) = entry.identity.ambient_agent_task_id {
ActionButtonsConfig::for_task(
task_id,
&entry.display.status,
open_action,
copy_link_url,
)
} else if let Some(conversation_id) = entry.identity.local_conversation_id {
ActionButtonsConfig::for_conversation(conversation_id, open_action, copy_link_url)
} else {
ActionButtonsConfig {
open_action,
copy_link_url,
..Default::default()
}
}
}
fn handle_action_buttons_event(
&mut self,
item_id: &ManagementCardItemId,
@@ -1109,6 +1131,7 @@ impl AgentManagementView {
summarize_after_fork: false,
summarization_prompt: None,
initial_prompt: None,
initial_attachments: vec![],
destination: ForkedConversationDestination::NewTab,
});
}
@@ -1122,7 +1145,7 @@ impl AgentManagementView {
);
self.update_details_panel_for_item(item_id, ctx);
self.selected_item_id = Some(item_id.clone());
self.selected_item_id = Some(*item_id);
ctx.notify();
}
AgentDetailsButtonEvent::CopyLink { link } => {
@@ -1136,7 +1159,7 @@ impl AgentManagementView {
ctx
);
}
ManagementCardItemId::Task(task_id) => {
ManagementCardItemId::AmbientRun(task_id) => {
send_telemetry_from_ctx!(
AgentManagementTelemetryEvent::SessionLinkCopied {
task_id: task_id.to_string(),
@@ -1237,13 +1260,9 @@ impl AgentManagementView {
self.refresh_details_panel_if_needed(ctx);
self.get_tasks_from_model(ctx);
}
AgentConversationsModelEvent::ConversationUpdated => {
self.get_tasks_from_model(ctx);
self.refresh_details_panel_if_needed(ctx);
ctx.notify();
AgentConversationsModelEvent::ConversationUpdated { kind } => {
self.handle_conversation_updated(*kind, ctx);
}
// TaskManuallyOpened is handled by the conversation list view, not here.
AgentConversationsModelEvent::TaskManuallyOpened => {}
AgentConversationsModelEvent::ConversationArtifactsUpdated { conversation_id } => {
self.update_artifacts_for_conversation(*conversation_id, ctx);
self.refresh_details_panel_if_needed(ctx);
@@ -1253,11 +1272,48 @@ impl AgentManagementView {
/// Refresh the details panel if it's currently showing an item
fn refresh_details_panel_if_needed(&mut self, ctx: &mut ViewContext<Self>) {
if let Some(item_id) = self.selected_item_id.clone() {
if let Some(item_id) = self.selected_item_id {
self.update_details_panel_for_item(&item_id, ctx);
}
}
/// Decide how much work a `ConversationUpdated` event requires, based on its kind and the
/// active status filter:
/// * `Restored`: the underlying status didn't change, so the visible cards don't change
/// either. Just refresh the details panel.
/// * `MetadataChanged`: rebuild the cards so metadata-derived actions update.
/// * `TitleChanged`: rebuild the cards so filtering and titles update.
/// * `StatusSet` that crosses the active status filter: rebuild the
/// card list via `get_tasks_from_model`.
/// * `StatusSet` that doesn't cross the active filter (or `All` is active):
/// just refresh the details panel re-render so the status icon picks up the new value.
fn handle_conversation_updated(
&mut self,
kind: ConversationUpdateKind,
ctx: &mut ViewContext<Self>,
) {
match kind {
ConversationUpdateKind::Restored => {}
ConversationUpdateKind::MetadataChanged => self.get_tasks_from_model(ctx),
ConversationUpdateKind::TitleChanged => self.get_tasks_from_model(ctx),
ConversationUpdateKind::StatusSet {
prev_filter,
new_filter,
} => {
if self
.filters
.status
.is_membership_crossed(prev_filter, new_filter)
{
self.get_tasks_from_model(ctx);
} else {
ctx.notify();
}
}
}
self.refresh_details_panel_if_needed(ctx);
}
/// Update the details panel with fresh data for the given item.
fn update_details_panel_for_item(
&mut self,
@@ -1265,75 +1321,28 @@ impl AgentManagementView {
ctx: &mut ViewContext<Self>,
) {
let model = AgentConversationsModel::as_ref(ctx);
let data = match item_id {
ManagementCardItemId::Task(task_id) => {
let Some(task_wrapper) = model.get_task(task_id) else {
return;
};
// Agent management view should always open in a new tab
let open_action =
task_wrapper.get_open_action(Some(RestoreConversationLayout::NewTab), ctx);
let copy_link_url = task_wrapper.session_or_conversation_link(ctx);
let Some(task) = model.get_task_data(task_id) else {
return;
};
ConversationDetailsData::from_task(&task, open_action, copy_link_url, ctx)
}
ManagementCardItemId::Conversation(conversation_id) => {
let Some(conversation) = model.get_conversation(conversation_id) else {
return;
};
// Agent management view should always open in a new tab
let open_action =
conversation.get_open_action(Some(RestoreConversationLayout::NewTab), ctx);
let history_model = BlocklistAIHistoryModel::as_ref(ctx);
let ai_conversation = conversation
.navigation_data()
.and_then(|nav| history_model.conversation(&nav.id));
let server_conv_id = ai_conversation
.and_then(|c| c.server_conversation_token())
.map(|t| t.as_str().to_string())
.or_else(|| {
conversation
.navigation_data()
.and_then(|nav| history_model.get_conversation_metadata(&nav.id))
.and_then(|m| m.server_conversation_token.as_ref())
.map(|t| t.as_str().to_string())
});
let artifacts = ai_conversation
.map(|c| c.artifacts().to_vec())
.unwrap_or_default();
let status = Some(conversation.status(ctx));
let navigation_data = conversation.navigation_data();
let copy_link_url = conversation.session_or_conversation_link(ctx);
// Prefer server-reported harness when available; otherwise treat as a pure
// local conversation (always Warp Agent).
let harness = navigation_data
.and_then(|nav| history_model.get_server_conversation_metadata(&nav.id))
.map(|m| Harness::from(m.harness))
.or(Some(Harness::Oz));
ConversationDetailsData::from_conversation_metadata(
*conversation_id,
conversation.title(ctx),
conversation.creator_name(ctx),
conversation.created_at().with_timezone(&chrono::Local),
navigation_data.and_then(|n| n.initial_working_directory.clone()),
conversation.request_usage(ctx),
server_conv_id,
artifacts,
open_action,
status,
navigation_data.and_then(|n| n.initial_query.clone()),
copy_link_url,
harness,
)
}
let Some(entry) = model.get_entry_by_id(item_id, ctx) else {
return;
};
let open_action = AgentConversationsModel::resolve_open_action(
AgentConversationNavigationSubject::Entry(*item_id),
Some(RestoreConversationLayout::NewTab),
ctx,
);
let copy_link_url = AgentConversationsModel::resolve_copy_link(
AgentConversationNavigationSubject::Entry(*item_id),
ctx,
);
let task = entry
.identity
.ambient_agent_task_id
.and_then(|task_id| model.get_task_data(&task_id));
let data = ConversationDetailsData::from_agent_conversation_entry(
&entry,
task.as_ref(),
open_action,
copy_link_url,
);
self.details_panel.update(ctx, |p, ctx| {
p.set_conversation_details(data, ctx);
@@ -1347,19 +1356,14 @@ impl AgentManagementView {
ctx: &mut ViewContext<Self>,
) {
let model = AgentConversationsModel::as_ref(ctx);
let Some(card_data) = model.get_conversation(&conversation_id) else {
return;
};
let artifacts = card_data.artifacts(ctx);
// Find the index of the card for this conversation
let Some(index) = self
.items
.iter()
.position(|card| card.item_id == ManagementCardItemId::Conversation(conversation_id))
else {
let Some((index, entry)) = self.items.iter().enumerate().find_map(|(index, card)| {
let entry = model.get_entry_by_id(&card.item_id, ctx)?;
(entry.identity.local_conversation_id == Some(conversation_id))
.then_some((index, entry))
}) else {
return;
};
let artifacts = entry.display.artifacts;
// Update the artifact buttons for this card
if should_show_artifacts(&artifacts) {
@@ -1497,7 +1501,7 @@ impl AgentManagementView {
fn render_session_status_label(
appearance: &Appearance,
mouse_state: MouseStateHandle,
session_status: SessionStatus,
session_status: &SessionStatus,
) -> Box<dyn Element> {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
@@ -1604,21 +1608,17 @@ impl AgentManagementView {
};
let model = AgentConversationsModel::as_ref(app);
let card_data = match &card_state.item_id {
ManagementCardItemId::Task(task_id) => model.get_task(task_id),
ManagementCardItemId::Conversation(conv_id) => model.get_conversation(conv_id),
};
let Some(card_data) = card_data else {
let Some(entry) = model.get_entry_by_id(&card_state.item_id, app) else {
return Empty::new().finish();
};
self.render_card(card_state, &card_data, appearance, app)
self.render_card(card_state, &entry, appearance, app)
}
fn render_card(
&self,
card_state: &CardState,
card_data: &ConversationOrTask,
entry: &AgentConversationEntry,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
@@ -1642,10 +1642,8 @@ impl AgentManagementView {
let card_hoverable = Hoverable::new(card_state.hover_state.clone(), move |mouse_state| {
let mut card_content = Flex::column()
.with_spacing(CARD_ROW_SPACING)
.with_child(Self::render_header_row(
card_state, card_data, appearance, app,
))
.with_child(Self::render_metadata_row(card_data, appearance, app));
.with_child(Self::render_header_row(card_state, entry, appearance))
.with_child(Self::render_metadata_row(entry, appearance, app));
// Add artifacts row if there is a buttons view
if let Some(buttons_element) = artifact_buttons_element {
@@ -1706,18 +1704,12 @@ impl AgentManagementView {
})
.with_defer_events_to_children();
// Add click handler to open session if available
let item_id = card_state.item_id.clone();
let card_hoverable = if card_data
.get_open_action(Some(RestoreConversationLayout::NewTab), app)
.is_some()
{
let item_id = card_state.item_id;
let card_hoverable = if entry.capabilities.can_open {
card_hoverable
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(AgentManagementViewAction::OpenSession {
item_id: item_id.clone(),
});
ctx.dispatch_typed_action(AgentManagementViewAction::OpenSession { item_id });
})
} else {
card_hoverable
@@ -1728,29 +1720,30 @@ impl AgentManagementView {
fn render_header_row(
card_state: &CardState,
card_data: &ConversationOrTask,
entry: &AgentConversationEntry,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
let font_size = appearance.ui_font_size();
let title = card_data.title(app);
let title_text = Text::new_inline(title, font_family, font_size)
let title_text = Text::new_inline(entry.display.title.clone(), font_family, font_size)
.with_color(theme.active_ui_text_color().into());
let status_icon =
render_status_element(&card_data.display_status(app), STATUS_ICON_SIZE, appearance);
// Build the time and avatar elements
let last_updated = card_data.last_updated();
let time_str = format_approx_duration_from_now_utc(last_updated);
let status_icon = render_icon_with_status(
agent_conversation_entry_icon_variant(entry),
CARD_AGENT_ICON_SIZE,
0.,
theme,
internal_colors::fg_overlay_1(theme),
);
let time_str = format_approx_duration_from_now_utc(entry.display.last_updated);
let time_text = Text::new_inline(time_str, font_family, font_size)
.with_color(theme.nonactive_ui_text_color().into());
let creator_name = card_data
.creator_name(app)
let creator_name = entry
.display
.creator
.name
.clone()
.unwrap_or_else(|| "Unknown".to_string());
let avatar = Self::render_avatar_with_tooltip(
&creator_name,
@@ -1763,12 +1756,11 @@ impl AgentManagementView {
.with_spacing(2.)
.with_child(Container::new(status_icon).with_margin_right(4.).finish())
.with_child(Expanded::new(1., title_text.finish()).finish());
let mut time_and_avatar = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_spacing(4.);
if let Some(session_status) = card_data.get_session_status() {
if let Some(session_status) = &entry.display.session_status {
time_and_avatar.add_child(Self::render_session_status_label(
appearance,
card_state.session_status_hover_state.clone(),
@@ -1792,45 +1784,56 @@ impl AgentManagementView {
}
fn render_metadata_row(
card_data: &ConversationOrTask,
entry: &AgentConversationEntry,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
let font_size = appearance.ui_font_size();
// Build metadata parts conditionally
let mut metadata_parts = Vec::new();
if let Some(source) = card_data.source() {
if let Some(source) = &entry.display.source {
metadata_parts.push(format!("Source: {}", source.display_name()));
}
if FeatureFlag::AgentHarness.is_enabled() {
if let Some(harness) = card_data.harness() {
let availability = HarnessAvailabilityModel::as_ref(app);
if availability.should_show_harness_selector() {
if let Some(harness) = entry.display.harness {
metadata_parts.push(format!(
"Harness: {}",
harness_display::display_name(harness)
availability.display_name_for(harness)
));
}
}
if let Some(run_time) = card_data.run_time() {
if let Some(executor) = &entry.display.executor {
let same_as_creator =
executor.uid.is_some() && executor.uid == entry.display.creator.uid;
if !same_as_creator {
if let Some(name) = executor.name.as_deref().or(executor.uid.as_deref()) {
let label = if executor
.principal_type
.is_some_and(|pt| pt.is_service_account())
{
"Agent"
} else {
"Executor"
};
metadata_parts.push(format!("{label}: {name}"));
}
}
}
if let Some(run_time) = &entry.display.run_time {
metadata_parts.push(format!("Run time: {run_time}"));
}
if let Some(usage) = card_data.display_request_usage(app) {
if let Some(usage) = entry.display.request_usage.map(format_credits) {
metadata_parts.push(format!("Credits used: {usage}"));
}
if let Some(tokens) = card_data.display_total_tokens(app) {
metadata_parts.push(format!("Tokens: {tokens}"));
}
let metadata_text = metadata_parts.join(" • ");
Text::new(metadata_text, font_family, font_size)
Text::new(metadata_parts.join(" • "), font_family, font_size)
.with_color(theme.nonactive_ui_text_color().into())
.finish()
}
@@ -1947,7 +1950,7 @@ impl AgentManagementView {
.with_child(ChildView::new(&self.created_on_dropdown).finish())
.with_child(ChildView::new(&self.artifact_dropdown).finish());
if FeatureFlag::AgentHarness.is_enabled() {
if HarnessAvailabilityModel::as_ref(app).should_show_harness_selector() {
filters_wrap.add_child(ChildView::new(&self.harness_dropdown).finish());
}
@@ -2356,17 +2359,11 @@ impl TypedActionView for AgentManagementView {
ctx.notify();
}
AgentManagementViewAction::OpenSession { item_id } => {
let model = AgentConversationsModel::as_ref(ctx);
let card_data = match item_id {
ManagementCardItemId::Task(task_id) => model.get_task(task_id),
ManagementCardItemId::Conversation(conv_id) => model.get_conversation(conv_id),
};
let Some(card_data) = card_data else {
return;
};
let Some(action) =
card_data.get_open_action(Some(RestoreConversationLayout::NewTab), ctx)
else {
let Some(action) = AgentConversationsModel::resolve_open_action(
AgentConversationNavigationSubject::Entry(*item_id),
Some(RestoreConversationLayout::NewTab),
ctx,
) else {
return;
};
@@ -2380,7 +2377,7 @@ impl TypedActionView for AgentManagementView {
ctx
);
}
ManagementCardItemId::Task(task_id) => {
ManagementCardItemId::AmbientRun(task_id) => {
send_telemetry_from_ctx!(
AgentManagementTelemetryEvent::CloudRunOpened {
task_id: task_id.to_string(),