Keep AI session history local and update Galaxy branding
This commit is contained in:
@@ -214,6 +214,8 @@ Key invariants:
|
||||
- Direct-provider normal and plan turns must advertise `read_plan`, `create_plan`, and `edit_plan` when the matching document capabilities are enabled; plan-creation requests should call `create_plan` after research rather than only returning prose
|
||||
- Direct-provider normal and plan turns expose `update_task_list` for explicit progress updates. Keep the validated current list in durable `ProviderRun` state, inject it into each request, preserve it across compaction and restore, and reconcile restored UI updates idempotently. Finishing a response must never mark unfinished work complete. CLI monitors and completed-command assessments retain their focused tool sets.
|
||||
- Completed local plans are archived intact into `Plans/Archive` after seven days only when their conversation has explicitly completed all checklist items and has no active provider run or child. Draft, unfinished, or reopened plans stay visible; resumed work restores an automatically archived plan, and manual recovery is respected. Archival never deletes plan contents.
|
||||
- Session history lists conversations available in local panes or local persistence, with local status/date/search filters. Restore from memory or SQLite only; never fall back to hosted attachment or server recovery, and report unavailable local data without substituting a new conversation. Ignore stale cloud/team/source/harness/artifact filters on this screen.
|
||||
- Active UI and built-in agent instructions use Galaxy branding. Preserve legacy serialized names, protocol identifiers, environment variables, actual third-party package names, and upstream service references where compatibility requires them; do not relabel an unsupported hosted service as a Galaxy feature.
|
||||
- Unknown or invalid tool calls receive one correlated synthetic error result and a visible `AgentOutput` message; the durable run owns any continuation
|
||||
- `recall_tool_history` is an inline completed tool batch. `ProviderRun` commits its synthetic result and starts a bounded next turn without routing it through client action execution
|
||||
- `recall_tool_history` excludes earlier calls to itself; archived tool results remain searchable by query or exact `tool_use_id`
|
||||
|
||||
@@ -754,7 +754,7 @@ pub enum RenderableAIError {
|
||||
|
||||
impl RenderableAIError {
|
||||
const TRANSIENT_NETWORK_ERROR_MESSAGE: &'static str =
|
||||
"Warp lost connection while receiving the agent response. This is usually temporary.";
|
||||
"Galaxy lost connection while receiving the agent response. This is usually temporary.";
|
||||
/// User-facing message shown when an agent-issued command exits the shell.
|
||||
pub const AGENT_EXITED_SHELL_MESSAGE: &'static str =
|
||||
"The shell exited while the agent was running a command, so the run could not continue. Ensure the agent is not asked to run commands or source scripts that can exit the shell.";
|
||||
@@ -905,9 +905,9 @@ impl Display for RenderableAIError {
|
||||
}
|
||||
}
|
||||
Self::ServerOverloaded => {
|
||||
write!(f, "Warp is currently overloaded. Please try again later.")
|
||||
write!(f, "The AI service is currently overloaded. Please try again later.")
|
||||
}
|
||||
Self::InternalWarpError => write!(f, "Internal Warp error."),
|
||||
Self::InternalWarpError => write!(f, "Internal Galaxy error."),
|
||||
Self::ContextWindowExceeded(message) => {
|
||||
write!(f, "Context window exceeded: {message}")
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ fn transient_network_error_includes_user_facing_message_and_debug_details() {
|
||||
let rendered = error.to_string();
|
||||
assert!(
|
||||
rendered.starts_with(
|
||||
"Warp lost connection while receiving the agent response. This is usually temporary.\n\nDebug info: "
|
||||
"Galaxy lost connection while receiving the agent response. This is usually temporary.\n\nDebug info: "
|
||||
),
|
||||
"unexpected rendering: {rendered}"
|
||||
);
|
||||
|
||||
@@ -284,6 +284,16 @@ pub struct AgentManagementFilters {
|
||||
}
|
||||
|
||||
impl AgentManagementFilters {
|
||||
/// Retain only filters supported by the local session history screen.
|
||||
/// Older app state may still contain cloud-only filter selections.
|
||||
pub fn for_local_history(&self) -> Self {
|
||||
Self {
|
||||
status: self.status,
|
||||
created_on: self.created_on,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_all_but_owner(&mut self) {
|
||||
self.status = StatusFilter::default();
|
||||
self.source = SourceFilter::default();
|
||||
@@ -1204,6 +1214,98 @@ impl AgentConversationsModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns locally recoverable sessions without hosted run or server-only rows.
|
||||
pub fn get_local_entries(
|
||||
&self,
|
||||
filters: &AgentManagementFilters,
|
||||
app: &AppContext,
|
||||
) -> Vec<AgentConversationEntry> {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let filters = filters.for_local_history();
|
||||
let mut entries = HashMap::new();
|
||||
|
||||
for metadata in self.conversations.values() {
|
||||
let entry = entry::entry_for_conversation(metadata, history_model, app);
|
||||
// Metadata from a live local session can precede its first disk write.
|
||||
if !entry.backing.has_ambient_run
|
||||
&& (entry.backing.has_loaded_conversation
|
||||
|| entry.backing.has_local_persisted_data
|
||||
|| !entry.backing.has_cloud_data)
|
||||
{
|
||||
entries.insert(entry.id, entry.for_local_history());
|
||||
}
|
||||
}
|
||||
|
||||
for metadata in history_model.get_local_conversations_metadata() {
|
||||
if !metadata.has_local_data && history_model.conversation(&metadata.id).is_none() {
|
||||
continue;
|
||||
}
|
||||
let id = AgentConversationEntryId::Conversation(metadata.id);
|
||||
entries.entry(id).or_insert_with(|| {
|
||||
let nav_data =
|
||||
ConversationNavigationData::from_historical_conversation_metadata(metadata);
|
||||
entry::entry_for_historical_metadata(metadata, nav_data, history_model, app)
|
||||
.for_local_history()
|
||||
});
|
||||
}
|
||||
|
||||
entries
|
||||
.into_values()
|
||||
.filter(|entry| entry.matches_filters(&filters, app))
|
||||
.sorted_by(|a, b| b.display.last_updated.cmp(&a.display.last_updated))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_local_entry_by_id(
|
||||
&self,
|
||||
id: &AgentConversationEntryId,
|
||||
app: &AppContext,
|
||||
) -> Option<AgentConversationEntry> {
|
||||
let AgentConversationEntryId::Conversation(conversation_id) = id else {
|
||||
return None;
|
||||
};
|
||||
let entry = self.get_entry_by_id(id, app)?;
|
||||
(!entry.backing.has_ambient_run
|
||||
&& (entry.backing.has_loaded_conversation
|
||||
|| entry.backing.has_local_persisted_data
|
||||
|| (!entry.backing.has_cloud_data
|
||||
&& self.conversations.contains_key(conversation_id))))
|
||||
.then(|| entry.for_local_history())
|
||||
}
|
||||
|
||||
/// Resolve history navigation exclusively through an existing local pane or disk data.
|
||||
pub fn resolve_local_open_action(
|
||||
id: AgentConversationEntryId,
|
||||
restore_layout: Option<RestoreConversationLayout>,
|
||||
app: &AppContext,
|
||||
) -> Option<WorkspaceAction> {
|
||||
let model = Self::as_ref(app);
|
||||
let entry = model.get_local_entry_by_id(&id, app)?;
|
||||
let conversation_id = entry.identity.local_conversation_id?;
|
||||
let active_views = ActiveAgentViewsModel::as_ref(app);
|
||||
let nav_data = model
|
||||
.conversations
|
||||
.get(&conversation_id)
|
||||
.map(|metadata| &metadata.nav_data);
|
||||
|
||||
if active_views.is_conversation_open(conversation_id, app) && nav_data.is_none() {
|
||||
return active_views
|
||||
.get_terminal_view_id_for_conversation(conversation_id, app)
|
||||
.map(|terminal_view_id| WorkspaceAction::FocusTerminalViewInWorkspace {
|
||||
terminal_view_id,
|
||||
});
|
||||
}
|
||||
|
||||
Some(WorkspaceAction::RestoreOrNavigateToConversation {
|
||||
conversation_id,
|
||||
window_id: nav_data.and_then(|data| data.window_id),
|
||||
pane_view_locator: nav_data.and_then(|data| data.pane_view_locator),
|
||||
terminal_view_id: nav_data.and_then(|data| data.terminal_view_id),
|
||||
restore_layout,
|
||||
local_only: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns normalized, owned entries for agent management/navigation surfaces.
|
||||
pub fn get_entries(
|
||||
&self,
|
||||
@@ -1380,6 +1482,7 @@ impl AgentConversationsModel {
|
||||
pane_view_locator: nav_data.pane_view_locator,
|
||||
terminal_view_id: nav_data.terminal_view_id,
|
||||
restore_layout,
|
||||
local_only: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1409,6 +1512,7 @@ impl AgentConversationsModel {
|
||||
pane_view_locator: None,
|
||||
terminal_view_id: nav_data.and_then(|nav_data| nav_data.terminal_view_id),
|
||||
restore_layout,
|
||||
local_only: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,26 @@ pub struct AgentConversationCapabilities {
|
||||
}
|
||||
|
||||
impl AgentConversationEntry {
|
||||
/// Local history never offers hosted attachment, sharing, or server recovery.
|
||||
pub(super) fn for_local_history(mut self) -> Self {
|
||||
self.provenance = AgentConversationProvenance::LocalInteractive;
|
||||
self.identity.ambient_agent_task_id = None;
|
||||
self.identity.server_conversation_token = None;
|
||||
self.identity.session_id = None;
|
||||
self.display.session_status = None;
|
||||
self.display.source = None;
|
||||
self.display.environment_id = None;
|
||||
self.display.harness = None;
|
||||
self.display.request_usage = None;
|
||||
self.display.executor = None;
|
||||
self.capabilities.can_open = true;
|
||||
self.capabilities.can_copy_link = false;
|
||||
self.capabilities.can_share = false;
|
||||
self.capabilities.can_fork_locally = false;
|
||||
self.capabilities.can_cancel = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn matches_filters(
|
||||
&self,
|
||||
filters: &AgentManagementFilters,
|
||||
|
||||
@@ -16,7 +16,8 @@ use super::entry::{
|
||||
use super::{
|
||||
record_earliest_rtc_task_refresh_timestamp, AgentConversationsModel,
|
||||
AgentConversationsModelEvent, AgentManagementFilters, AgentRunDisplayStatus, ArtifactFilter,
|
||||
ConversationMetadata, ConversationUpdateKind, EnvironmentFilter, HarnessFilter, OwnerFilter,
|
||||
ConversationMetadata, ConversationUpdateKind, CreatedOnFilter, CreatorFilter,
|
||||
EnvironmentFilter, HarnessFilter, OwnerFilter, SourceFilter,
|
||||
RtcTaskRefreshThrottleState, StatusFilter, TaskFetchError, TaskFetchState, MAX_PERSONAL_TASKS,
|
||||
MAX_TEAM_TASKS,
|
||||
};
|
||||
@@ -28,11 +29,12 @@ use crate::ai::agent::conversation::{
|
||||
};
|
||||
use crate::ai::ambient_agents::task::{HarnessConfig, TaskPrincipalInfo, TaskStatusMessage};
|
||||
use crate::ai::ambient_agents::{
|
||||
AgentConfigSnapshot, AmbientAgentTask, AmbientAgentTaskId, AmbientAgentTaskState,
|
||||
AgentConfigSnapshot, AgentSource, AmbientAgentTask, AmbientAgentTaskId, AmbientAgentTaskState,
|
||||
};
|
||||
use crate::ai::artifacts::Artifact;
|
||||
use crate::ai::blocklist::history_model::{
|
||||
BlocklistAIHistoryEvent, BlocklistAIHistoryModel, ConversationStatusUpdate,
|
||||
BlocklistAIHistoryEvent, BlocklistAIHistoryModel, CloudConversationData,
|
||||
ConversationStatusUpdate,
|
||||
};
|
||||
use crate::ai::conversation_navigation::ConversationNavigationData;
|
||||
use crate::auth::AuthStateProvider;
|
||||
@@ -814,6 +816,216 @@ fn create_server_conversation_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_history_discards_obsolete_filters_but_preserves_status_and_date() {
|
||||
let filters = AgentManagementFilters {
|
||||
owners: OwnerFilter::All,
|
||||
status: StatusFilter::Failed,
|
||||
source: SourceFilter::Specific(AgentSource::CloudMode),
|
||||
created_on: CreatedOnFilter::LastWeek,
|
||||
creator: CreatorFilter::Specific {
|
||||
name: "Former team".to_string(),
|
||||
uid: "former-user".to_string(),
|
||||
},
|
||||
artifact: ArtifactFilter::PullRequest,
|
||||
environment: EnvironmentFilter::Specific("old-environment".to_string()),
|
||||
harness: HarnessFilter::Specific(Harness::Oz),
|
||||
};
|
||||
assert_eq!(
|
||||
filters.for_local_history(),
|
||||
AgentManagementFilters {
|
||||
status: StatusFilter::Failed,
|
||||
created_on: CreatedOnFilter::LastWeek,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_history_keeps_saved_legacy_sessions_and_excludes_hosted_rows() {
|
||||
App::test((), |mut app| async move {
|
||||
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
|
||||
app.add_singleton_model(|_| ActiveAgentViewsModel::new());
|
||||
let conversation_id = AIConversationId::new();
|
||||
let task_id = "saved-root-task";
|
||||
let saved = persistence::model::AgentConversation {
|
||||
conversation: persistence::model::AgentConversationRecord {
|
||||
id: 0,
|
||||
conversation_id: conversation_id.to_string(),
|
||||
conversation_data: serde_json::to_string(&AgentConversationData {
|
||||
server_conversation_token: Some("legacy-saved-token".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap(),
|
||||
last_modified_at: Utc::now().naive_utc(),
|
||||
},
|
||||
tasks: vec![create_api_task(
|
||||
task_id,
|
||||
vec![create_message("saved-message", task_id)],
|
||||
)],
|
||||
};
|
||||
let history =
|
||||
app.add_singleton_model(|_| BlocklistAIHistoryModel::new(vec![], &[saved]));
|
||||
history.update(&mut app, |model, _| {
|
||||
model.merge_cloud_conversation_metadata(vec![create_server_conversation_metadata(
|
||||
"Remote-only conversation",
|
||||
"remote-only-token",
|
||||
None,
|
||||
)]);
|
||||
});
|
||||
app.add_singleton_model(|_| {
|
||||
let mut model = create_test_model();
|
||||
let task = create_test_task(&make_uuid(8400), "user-a", Utc::now());
|
||||
model.tasks.insert(task.task_id, task);
|
||||
model
|
||||
});
|
||||
app.update(|ctx| {
|
||||
let model = AgentConversationsModel::as_ref(ctx);
|
||||
let entries = model.get_local_entries(&AgentManagementFilters::default(), ctx);
|
||||
assert_eq!(entries.len(), 1);
|
||||
let entry = &entries[0];
|
||||
assert_eq!(entry.identity.local_conversation_id, Some(conversation_id));
|
||||
assert!(entry.identity.server_conversation_token.is_none());
|
||||
assert!(entry.display.harness.is_none());
|
||||
assert!(!entry.capabilities.can_share);
|
||||
assert!(!entry.capabilities.can_copy_link);
|
||||
assert!(model.get_local_entry_by_id(&entry.id, ctx).is_some());
|
||||
assert!(matches!(
|
||||
AgentConversationsModel::resolve_local_open_action(entry.id, None, ctx),
|
||||
Some(WorkspaceAction::RestoreOrNavigateToConversation {
|
||||
conversation_id: restored_id,
|
||||
local_only: true,
|
||||
..
|
||||
}) if restored_id == conversation_id
|
||||
));
|
||||
let remote_id = history
|
||||
.as_ref(ctx)
|
||||
.find_conversation_id_by_server_token(&ServerConversationToken::new(
|
||||
"remote-only-token".to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
assert!(AgentConversationsModel::resolve_local_open_action(
|
||||
AgentConversationEntryId::Conversation(remote_id), None, ctx,
|
||||
).is_none());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_history_loader_does_not_fetch_missing_cloud_data() {
|
||||
App::test((), |mut app| async move {
|
||||
add_entry_projection_test_models(&mut app);
|
||||
let history = BlocklistAIHistoryModel::handle(&app);
|
||||
history.update(&mut app, |model, _| {
|
||||
model.merge_cloud_conversation_metadata(vec![create_server_conversation_metadata(
|
||||
"Remote-only conversation",
|
||||
"remote-only-token",
|
||||
None,
|
||||
)]);
|
||||
});
|
||||
let future = app.update(|ctx| {
|
||||
let model = history.as_ref(ctx);
|
||||
let id = model
|
||||
.find_conversation_id_by_server_token(&ServerConversationToken::new(
|
||||
"remote-only-token".to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
model.load_local_conversation_data(id)
|
||||
});
|
||||
// No ServerApiProvider is installed: any accidental server fallback would fail.
|
||||
assert!(future.await.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_history_recovers_current_live_state_without_hosted_task_shadowing() {
|
||||
App::test((), |mut app| async move {
|
||||
add_entry_projection_test_models(&mut app);
|
||||
let conversation_id = AIConversationId::new();
|
||||
let run_id = make_uuid(8401);
|
||||
let conversation = create_restored_conversation(
|
||||
conversation_id,
|
||||
"live-root-task",
|
||||
AgentConversationData {
|
||||
run_id: Some(run_id.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let history = BlocklistAIHistoryModel::handle(&app);
|
||||
history.update(&mut app, |model, ctx| {
|
||||
model.restore_conversations(EntityId::new(), vec![conversation], ctx);
|
||||
model
|
||||
.conversation_mut(&conversation_id)
|
||||
.unwrap()
|
||||
.set_last_event_sequence(42);
|
||||
});
|
||||
let mut model = create_test_model();
|
||||
let task = create_test_task(&run_id, "user-a", Utc::now());
|
||||
model.tasks.insert(task.task_id, task);
|
||||
let future = app.update(|ctx| {
|
||||
let entries = model.get_local_entries(&AgentManagementFilters::default(), ctx);
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(
|
||||
entries[0].id,
|
||||
AgentConversationEntryId::Conversation(conversation_id)
|
||||
);
|
||||
assert!(entries[0].identity.ambient_agent_task_id.is_none());
|
||||
assert!(model.get_local_entry_by_id(&entries[0].id, ctx).is_some());
|
||||
history
|
||||
.as_ref(ctx)
|
||||
.load_local_conversation_data(conversation_id)
|
||||
});
|
||||
let Some(CloudConversationData::Oz(restored)) = future.await else {
|
||||
panic!("live local session should restore without disk or server access");
|
||||
};
|
||||
assert_eq!(restored.id(), conversation_id);
|
||||
assert_eq!(restored.last_event_sequence(), Some(42));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_history_excludes_cached_ambient_sessions_from_list_and_lookup() {
|
||||
App::test((), |mut app| async move {
|
||||
add_entry_projection_test_models(&mut app);
|
||||
let conversation_id = AIConversationId::new();
|
||||
let task_id = make_uuid(8402).parse().unwrap();
|
||||
let mut conversation = create_restored_conversation(
|
||||
conversation_id,
|
||||
"ambient-root-task",
|
||||
AgentConversationData::default(),
|
||||
);
|
||||
conversation.set_server_metadata(create_server_conversation_metadata(
|
||||
"Cached hosted run",
|
||||
"cached-ambient-token",
|
||||
Some(task_id),
|
||||
));
|
||||
let history = BlocklistAIHistoryModel::handle(&app);
|
||||
history.update(&mut app, |model, ctx| {
|
||||
model.restore_conversations(EntityId::new(), vec![conversation], ctx);
|
||||
});
|
||||
let mut model = create_test_model();
|
||||
model.conversations.insert(
|
||||
conversation_id,
|
||||
create_test_conversation_metadata(conversation_id, "Cached hosted run"),
|
||||
);
|
||||
let future = app.update(|ctx| {
|
||||
assert!(model
|
||||
.get_local_entries(&AgentManagementFilters::default(), ctx)
|
||||
.is_empty());
|
||||
assert!(model
|
||||
.get_local_entry_by_id(
|
||||
&AgentConversationEntryId::Conversation(conversation_id),
|
||||
ctx,
|
||||
)
|
||||
.is_none());
|
||||
history
|
||||
.as_ref(ctx)
|
||||
.load_local_conversation_data(conversation_id)
|
||||
});
|
||||
assert!(future.await.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_entries_includes_task_only_entry() {
|
||||
App::test((), |mut app| async move {
|
||||
|
||||
@@ -36,7 +36,7 @@ use siphasher::sip::SipHasher;
|
||||
|
||||
use crate::ai::agent::conversation::AIConversationId;
|
||||
use crate::ai::agent_conversations_model::{
|
||||
AgentConversationEntry, AgentConversationEntryId, AgentConversationNavigationSubject,
|
||||
AgentConversationEntry, AgentConversationEntryId,
|
||||
AgentConversationsModel, AgentConversationsModelEvent, AgentManagementFilters, ArtifactFilter,
|
||||
ConversationUpdateKind, CreatedOnFilter, CreatorFilter, EnvironmentFilter, HarnessFilter,
|
||||
OwnerFilter, SessionStatus, SourceFilter, StatusFilter,
|
||||
@@ -55,7 +55,6 @@ use crate::ai::agent_management::telemetry::{
|
||||
};
|
||||
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,
|
||||
};
|
||||
@@ -89,7 +88,6 @@ 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! {
|
||||
@@ -311,7 +309,7 @@ impl AgentManagementView {
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
editor.set_placeholder_text("Search", ctx);
|
||||
editor.set_placeholder_text("Search local sessions", ctx);
|
||||
editor
|
||||
});
|
||||
ctx.subscribe_to_view(&search_editor, |me, _handle, event, ctx| {
|
||||
@@ -319,7 +317,7 @@ impl AgentManagementView {
|
||||
});
|
||||
|
||||
let new_agent_button = CompactibleActionButton::new(
|
||||
"New agent".to_string(),
|
||||
"New session".to_string(),
|
||||
None,
|
||||
ButtonSize::Small,
|
||||
AgentManagementViewAction::ShowAgentTypeSelector,
|
||||
@@ -333,7 +331,9 @@ impl AgentManagementView {
|
||||
|
||||
let has_dismissed_setup_guide = *AISettings::as_ref(ctx).did_dismiss_cloud_setup_guide;
|
||||
|
||||
let filters = persisted_filters.map(|p| p.filters).unwrap_or_default();
|
||||
let filters = persisted_filters
|
||||
.map(|persisted| persisted.filters.for_local_history())
|
||||
.unwrap_or_default();
|
||||
|
||||
let details_panel: ViewHandle<ConversationDetailsPanel> =
|
||||
ctx.add_typed_action_view(|ctx| {
|
||||
@@ -378,28 +378,17 @@ impl AgentManagementView {
|
||||
view.update_creator_dropdown(ctx);
|
||||
view.update_environment_dropdown(ctx);
|
||||
|
||||
// Trigger server fetch if persisted filters differ from defaults
|
||||
// (team tasks are not loaded at startup, so we need to fetch them)
|
||||
if view.filters != AgentManagementFilters::default() {
|
||||
view.trigger_filter_fetch(ctx);
|
||||
}
|
||||
|
||||
view.get_tasks_from_model(ctx);
|
||||
view
|
||||
}
|
||||
|
||||
fn get_view_state(&self, app: &AppContext) -> ViewState {
|
||||
let model = AgentConversationsModel::as_ref(app);
|
||||
let has_items = model.has_items();
|
||||
let has_items = !model
|
||||
.get_local_entries(&AgentManagementFilters::default(), app)
|
||||
.is_empty();
|
||||
|
||||
// If loading with zero items, show skeleton cards
|
||||
// If loading with items, show list of interactive conversations (with loading indicator in header)
|
||||
if model.is_loading() && !has_items {
|
||||
return ViewState::Loading;
|
||||
}
|
||||
|
||||
// Show setup guide if: no items (zero state) or user clicked button to toggle on the guide
|
||||
if !has_items || self.is_viewing_setup_guide {
|
||||
if !has_items {
|
||||
return ViewState::SetupGuide { has_items };
|
||||
}
|
||||
|
||||
@@ -866,7 +855,7 @@ impl AgentManagementView {
|
||||
|
||||
/// Common handler for filter changes: fetch, refresh tasks, and save.
|
||||
fn on_filter_changed(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.trigger_filter_fetch(ctx);
|
||||
self.filters = self.filters.for_local_history();
|
||||
self.get_tasks_from_model(ctx);
|
||||
ctx.dispatch_global_action("workspace:save_app", ());
|
||||
}
|
||||
@@ -888,11 +877,9 @@ impl AgentManagementView {
|
||||
|
||||
/// Shows the setup guide from a deep-link/action without toggling it off on repeated calls.
|
||||
pub(crate) fn show_setup_guide_from_link(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
if !self.is_viewing_setup_guide {
|
||||
send_telemetry_from_ctx!(AgentManagementTelemetryEvent::OpenSetupGuide, ctx);
|
||||
}
|
||||
self.is_viewing_setup_guide = true;
|
||||
ctx.notify();
|
||||
// Older links may target cloud onboarding. Keep them within local history.
|
||||
self.is_viewing_setup_guide = false;
|
||||
self.get_tasks_from_model(ctx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -905,10 +892,10 @@ impl AgentManagementView {
|
||||
environment_id: String,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// This navigation should show the team/global task runs list.
|
||||
self.filters.owners = OwnerFilter::All;
|
||||
// A legacy environment link must not invisibly filter local sessions.
|
||||
let _ = environment_id;
|
||||
self.filters.owners = OwnerFilter::PersonalOnly;
|
||||
self.filters.reset_all_but_owner();
|
||||
self.filters.environment = EnvironmentFilter::Specific(environment_id);
|
||||
self.update_filter_buttons(ctx);
|
||||
|
||||
// Clear search query.
|
||||
@@ -952,7 +939,7 @@ impl AgentManagementView {
|
||||
let model = AgentConversationsModel::as_ref(ctx);
|
||||
let search_query = self.search_query.trim().to_lowercase();
|
||||
let cards: Vec<CardData> = model
|
||||
.get_entries(&self.filters, ctx)
|
||||
.get_local_entries(&self.filters, ctx)
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
if search_query.is_empty() {
|
||||
@@ -962,16 +949,7 @@ impl AgentManagementView {
|
||||
})
|
||||
.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,
|
||||
)
|
||||
})
|
||||
.flatten();
|
||||
let copy_link_url = None;
|
||||
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);
|
||||
@@ -1100,6 +1078,9 @@ impl AgentManagementView {
|
||||
{
|
||||
config.delete_conversation_id = None;
|
||||
}
|
||||
if !entry.capabilities.can_fork_locally {
|
||||
config.fork_conversation_id = None;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
@@ -1184,7 +1165,7 @@ impl AgentManagementView {
|
||||
AgentDetailsButtonEvent::DeleteConversation { conversation_id } => {
|
||||
let model = AgentConversationsModel::as_ref(ctx);
|
||||
let conversation_title = model
|
||||
.get_entry_by_id(item_id, ctx)
|
||||
.get_local_entry_by_id(item_id, ctx)
|
||||
.map(|entry| entry.display.title)
|
||||
.unwrap_or_else(|| "Conversation".to_string());
|
||||
ctx.emit(AgentManagementViewEvent::ShowDeleteConfirmationDialog {
|
||||
@@ -1341,18 +1322,15 @@ impl AgentManagementView {
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let model = AgentConversationsModel::as_ref(ctx);
|
||||
let Some(entry) = model.get_entry_by_id(item_id, ctx) else {
|
||||
let Some(entry) = model.get_local_entry_by_id(item_id, ctx) else {
|
||||
return;
|
||||
};
|
||||
let open_action = AgentConversationsModel::resolve_open_action(
|
||||
AgentConversationNavigationSubject::Entry(*item_id),
|
||||
let open_action = AgentConversationsModel::resolve_local_open_action(
|
||||
*item_id,
|
||||
Some(RestoreConversationLayout::NewTab),
|
||||
ctx,
|
||||
);
|
||||
let copy_link_url = AgentConversationsModel::resolve_copy_link(
|
||||
AgentConversationNavigationSubject::Entry(*item_id),
|
||||
ctx,
|
||||
);
|
||||
let copy_link_url = None;
|
||||
let task = entry
|
||||
.identity
|
||||
.ambient_agent_task_id
|
||||
@@ -1362,7 +1340,8 @@ impl AgentManagementView {
|
||||
task.as_ref(),
|
||||
open_action,
|
||||
copy_link_url,
|
||||
);
|
||||
)
|
||||
.for_local_history();
|
||||
|
||||
self.details_panel.update(ctx, |p, ctx| {
|
||||
p.set_conversation_details(data, ctx);
|
||||
@@ -1377,7 +1356,7 @@ impl AgentManagementView {
|
||||
) {
|
||||
let model = AgentConversationsModel::as_ref(ctx);
|
||||
let Some((index, entry)) = self.items.iter().enumerate().find_map(|(index, card)| {
|
||||
let entry = model.get_entry_by_id(&card.item_id, ctx)?;
|
||||
let entry = model.get_local_entry_by_id(&card.item_id, ctx)?;
|
||||
(entry.identity.local_conversation_id == Some(conversation_id))
|
||||
.then_some((index, entry))
|
||||
}) else {
|
||||
@@ -1638,7 +1617,7 @@ impl AgentManagementView {
|
||||
};
|
||||
|
||||
let model = AgentConversationsModel::as_ref(app);
|
||||
let Some(entry) = model.get_entry_by_id(&card_state.item_id, app) else {
|
||||
let Some(entry) = model.get_local_entry_by_id(&card_state.item_id, app) else {
|
||||
return Empty::new().finish();
|
||||
};
|
||||
|
||||
@@ -1673,7 +1652,7 @@ impl AgentManagementView {
|
||||
let mut card_content = Flex::column()
|
||||
.with_spacing(CARD_ROW_SPACING)
|
||||
.with_child(Self::render_header_row(card_state, entry, appearance))
|
||||
.with_child(Self::render_metadata_row(entry, appearance, app));
|
||||
.with_child(Self::render_metadata_row(entry, appearance));
|
||||
|
||||
// Add artifacts row if there is a buttons view
|
||||
if let Some(buttons_element) = artifact_buttons_element {
|
||||
@@ -1816,25 +1795,14 @@ impl AgentManagementView {
|
||||
fn render_metadata_row(
|
||||
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 mut metadata_parts = Vec::new();
|
||||
|
||||
if let Some(source) = &entry.display.source {
|
||||
metadata_parts.push(format!("Source: {}", source.display_name()));
|
||||
}
|
||||
|
||||
let availability = HarnessAvailabilityModel::as_ref(app);
|
||||
if availability.should_show_harness_selector() {
|
||||
if let Some(harness) = entry.display.harness {
|
||||
metadata_parts.push(format!(
|
||||
"Harness: {}",
|
||||
availability.display_name_for(harness)
|
||||
));
|
||||
}
|
||||
if let Some(directory) = &entry.display.working_directory {
|
||||
metadata_parts.push(directory.clone());
|
||||
}
|
||||
|
||||
if let Some(executor) = &entry.display.executor {
|
||||
@@ -1859,8 +1827,8 @@ impl AgentManagementView {
|
||||
metadata_parts.push(format!("Run time: {run_time}"));
|
||||
}
|
||||
|
||||
if let Some(usage) = entry.display.request_usage.map(format_credits) {
|
||||
metadata_parts.push(format!("Credits used: {usage}"));
|
||||
if metadata_parts.is_empty() {
|
||||
metadata_parts.push("Saved on this device".to_string());
|
||||
}
|
||||
|
||||
Text::new(metadata_parts.join(" • "), font_family, font_size)
|
||||
@@ -1870,13 +1838,7 @@ impl AgentManagementView {
|
||||
|
||||
// Render the main page header based on the current view state
|
||||
fn render_header(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
match self.get_view_state(app) {
|
||||
ViewState::Loading => self.render_loading_header(app),
|
||||
ViewState::SetupGuide {
|
||||
has_items: has_tasks,
|
||||
} => self.render_setup_guide_header(has_tasks, app),
|
||||
ViewState::NoFilterMatches | ViewState::HasTasks => self.render_task_list_header(app),
|
||||
}
|
||||
self.render_task_list_header(app)
|
||||
}
|
||||
|
||||
fn render_setup_guide_header(&self, has_tasks: bool, app: &AppContext) -> Box<dyn Element> {
|
||||
@@ -1920,17 +1882,11 @@ impl AgentManagementView {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
|
||||
let is_loading = AgentConversationsModel::handle(app)
|
||||
.as_ref(app)
|
||||
.is_loading();
|
||||
|
||||
let is_on_team = UserWorkspaces::as_ref(app).current_team().is_some();
|
||||
|
||||
let size_switch_threshold = MEDIUM_SIZE_SWITCH_THRESHOLD * appearance.monospace_ui_scalar();
|
||||
|
||||
let build_header = |use_expanded: bool| {
|
||||
let title = Text::new_inline(
|
||||
"Runs",
|
||||
"AI sessions",
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_size() + 4.,
|
||||
)
|
||||
@@ -1938,12 +1894,6 @@ impl AgentManagementView {
|
||||
.with_color(theme.active_ui_text_color().into())
|
||||
.finish();
|
||||
|
||||
let setup_guide_button = if use_expanded {
|
||||
self.setup_guide_button.expanded_button()
|
||||
} else {
|
||||
self.setup_guide_button.compact_button()
|
||||
};
|
||||
|
||||
let new_agent_button = if use_expanded {
|
||||
self.new_agent_button.expanded_button()
|
||||
} else {
|
||||
@@ -1956,17 +1906,7 @@ impl AgentManagementView {
|
||||
.with_spacing(4.)
|
||||
.with_child(Container::new(title).with_margin_right(12.).finish());
|
||||
|
||||
if is_on_team {
|
||||
header_top.add_child(ChildView::new(&self.personal_filter_button).finish());
|
||||
header_top.add_child(ChildView::new(&self.all_filter_button).finish());
|
||||
}
|
||||
|
||||
if is_loading {
|
||||
header_top.add_child(self.render_cloud_loading_icon(appearance));
|
||||
}
|
||||
|
||||
header_top.add_child(Expanded::new(1., Empty::new().finish()).finish());
|
||||
header_top.add_child(ChildView::new(setup_guide_button).finish());
|
||||
|
||||
if !cfg!(target_family = "wasm") {
|
||||
header_top.add_child(ChildView::new(new_agent_button).finish());
|
||||
@@ -1976,19 +1916,7 @@ impl AgentManagementView {
|
||||
.with_spacing(4.)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(ChildView::new(&self.status_dropdown).finish())
|
||||
.with_child(ChildView::new(&self.source_dropdown).finish())
|
||||
.with_child(ChildView::new(&self.created_on_dropdown).finish())
|
||||
.with_child(ChildView::new(&self.artifact_dropdown).finish());
|
||||
|
||||
if HarnessAvailabilityModel::as_ref(app).should_show_harness_selector() {
|
||||
filters_wrap.add_child(ChildView::new(&self.harness_dropdown).finish());
|
||||
}
|
||||
|
||||
filters_wrap.add_child(ChildView::new(&self.environment_dropdown).finish());
|
||||
|
||||
if self.filters.owners != OwnerFilter::PersonalOnly {
|
||||
filters_wrap.add_child(ChildView::new(&self.creator_dropdown).finish());
|
||||
}
|
||||
.with_child(ChildView::new(&self.created_on_dropdown).finish());
|
||||
|
||||
if self.filters.is_filtering() {
|
||||
filters_wrap.add_child(ChildView::new(&self.clear_all_filters_button).finish());
|
||||
@@ -2198,7 +2126,19 @@ impl View for AgentManagementView {
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let main_content = match self.get_view_state(app) {
|
||||
ViewState::Loading => self.render_loading_state(app),
|
||||
ViewState::SetupGuide { .. } => ChildView::new(&self.cloud_setup_guide_view).finish(),
|
||||
ViewState::SetupGuide { .. } => {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
Align::new(
|
||||
Text::new(
|
||||
"No saved AI sessions on this device. Start a session to keep its history here.",
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_size(),
|
||||
)
|
||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
ViewState::NoFilterMatches => self.render_no_results_view(app),
|
||||
ViewState::HasTasks => self.render_default_scroll_view(app),
|
||||
};
|
||||
@@ -2388,16 +2328,17 @@ impl TypedActionView for AgentManagementView {
|
||||
}
|
||||
AgentManagementViewAction::ShowAgentTypeSelector => {
|
||||
send_telemetry_from_ctx!(
|
||||
AgentManagementTelemetryEvent::AgentTypeSelectorOpened,
|
||||
AgentManagementTelemetryEvent::SpawnNewLocalAgent,
|
||||
ctx
|
||||
);
|
||||
self.is_agent_type_selector_open = true;
|
||||
ctx.focus(&self.agent_type_selector);
|
||||
ctx.notify();
|
||||
ctx.dispatch_typed_action(&WorkspaceAction::NewTabInAgentMode {
|
||||
entrypoint: AgentModeEntrypoint::AgentManagementView,
|
||||
zero_state_prompt_suggestion_type: None,
|
||||
});
|
||||
}
|
||||
AgentManagementViewAction::OpenSession { item_id } => {
|
||||
let Some(action) = AgentConversationsModel::resolve_open_action(
|
||||
AgentConversationNavigationSubject::Entry(*item_id),
|
||||
let Some(action) = AgentConversationsModel::resolve_local_open_action(
|
||||
*item_id,
|
||||
Some(RestoreConversationLayout::NewTab),
|
||||
ctx,
|
||||
) else {
|
||||
|
||||
@@ -41,7 +41,7 @@ const PARENT_BRIDGE_HOOK_OUTPUT_ACK_FILE_NAME: &str = "pending-hook-output.ack";
|
||||
const PARENT_BRIDGE_MAX_CONTEXT_CHARS_ENV: &str = "OZ_PARENT_MAX_CONTEXT_CHARS";
|
||||
const PARENT_BRIDGE_DEFAULT_MAX_CONTEXT_CHARS: usize = 6000;
|
||||
pub(super) const MESSAGE_BRIDGE_CONTEXT_PREAMBLE: &str =
|
||||
"Oz mailbox update for this child run.\nSource: lead agent\nContext type: user-level coordination messages\n";
|
||||
"Galaxy mailbox update for this child run.\nSource: lead agent\nContext type: user-level coordination messages\n";
|
||||
const PARENT_BRIDGE_REMAINING_MESSAGES_NOTE: &str =
|
||||
"\n\nAdditional lead agent mailbox messages remain queued and may be surfaced later.";
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ use crate::workspaces::user_workspaces::UserWorkspaces;
|
||||
pub(crate) enum BootstrapError {
|
||||
/// The PTY or shell process failed before the bootstrap script completed.
|
||||
/// When `reason` is `Some`, the message is
|
||||
/// "Shell spawn failed: {reason}. Check the Warp logs for details."
|
||||
/// "Shell spawn failed: {reason}. Check the Galaxy logs for details."
|
||||
/// When `reason` is `None`, it is
|
||||
/// "Shell spawn failed. Check the Warp logs for details."
|
||||
/// "Shell spawn failed. Check the Galaxy logs for details."
|
||||
PtySpawnFailed { reason: Option<String> },
|
||||
/// The bootstrap script did not complete within the expected time.
|
||||
TimedOut,
|
||||
@@ -58,16 +58,16 @@ impl std::fmt::Display for BootstrapError {
|
||||
BootstrapError::PtySpawnFailed { reason: Some(r) } => {
|
||||
write!(
|
||||
f,
|
||||
"Shell spawn failed: {r}. Check the Warp logs for details."
|
||||
"Shell spawn failed: {r}. Check the Galaxy logs for details."
|
||||
)
|
||||
}
|
||||
BootstrapError::PtySpawnFailed { reason: None } => {
|
||||
write!(f, "Shell spawn failed. Check the Warp logs for details.")
|
||||
write!(f, "Shell spawn failed. Check the Galaxy logs for details.")
|
||||
}
|
||||
BootstrapError::TimedOut => write!(
|
||||
f,
|
||||
"Terminal session did not start within the expected time. \
|
||||
Check the Warp logs for details."
|
||||
Check the Galaxy logs for details."
|
||||
),
|
||||
BootstrapError::InternalError => {
|
||||
write!(f, "An unexpected internal error occurred during bootstrap.")
|
||||
|
||||
@@ -112,7 +112,7 @@ static DEFAULT_TIPS: LazyLock<Vec<AgentTip>> = LazyLock::new(|| {
|
||||
kind: AgentTipKind::SlashCommands,
|
||||
},
|
||||
AgentTip {
|
||||
description: "<keybinding> to open the Command Palette and access Warp actions and shortcuts.".to_string(),
|
||||
description: "<keybinding> to open the Command Palette and access Galaxy actions and shortcuts.".to_string(),
|
||||
link: Some("https://docs.warp.dev/terminal/command-palette".to_string()),
|
||||
binding_name: Some(TOGGLE_COMMAND_PALETTE_KEYBINDING_NAME),
|
||||
action: Some(WorkspaceAction::OpenPalette {
|
||||
|
||||
@@ -443,7 +443,7 @@ impl AgentInputFooter {
|
||||
ActionButton::new("Enable notifications", InstallPluginButtonTheme)
|
||||
.with_icon(Icon::Download)
|
||||
.with_tooltip(
|
||||
"Install the Warp plugin to enable rich agent notifications within Warp",
|
||||
"Install the Galaxy plugin to enable rich agent notifications within Galaxy",
|
||||
)
|
||||
.with_size(cli_button_size)
|
||||
.with_tooltip_alignment(TooltipAlignment::Left)
|
||||
@@ -456,7 +456,7 @@ impl AgentInputFooter {
|
||||
let plugin_instructions_button = ctx.add_typed_action_view(|_ctx| {
|
||||
ActionButton::new("Notifications setup instructions", InstallPluginButtonTheme)
|
||||
.with_icon(Icon::Info)
|
||||
.with_tooltip("View instructions to install the Warp plugin")
|
||||
.with_tooltip("View instructions to install the Galaxy plugin")
|
||||
.with_size(cli_button_size)
|
||||
.with_tooltip_alignment(TooltipAlignment::Left)
|
||||
.with_adjoined_side(AdjoinedSide::Right)
|
||||
@@ -468,9 +468,9 @@ impl AgentInputFooter {
|
||||
});
|
||||
|
||||
let update_plugin_button = ctx.add_typed_action_view(|_ctx| {
|
||||
ActionButton::new("Update Warp plugin", InstallPluginButtonTheme)
|
||||
ActionButton::new("Update Galaxy plugin", InstallPluginButtonTheme)
|
||||
.with_icon(Icon::Download)
|
||||
.with_tooltip("A new version of the Warp plugin is available")
|
||||
.with_tooltip("A new version of the Galaxy plugin is available")
|
||||
.with_size(cli_button_size)
|
||||
.with_tooltip_alignment(TooltipAlignment::Left)
|
||||
.with_adjoined_side(AdjoinedSide::Right)
|
||||
@@ -482,7 +482,7 @@ impl AgentInputFooter {
|
||||
let update_instructions_button = ctx.add_typed_action_view(|_ctx| {
|
||||
ActionButton::new("Plugin update instructions", InstallPluginButtonTheme)
|
||||
.with_icon(Icon::Info)
|
||||
.with_tooltip("View instructions to update the Warp plugin")
|
||||
.with_tooltip("View instructions to update the Galaxy plugin")
|
||||
.with_size(cli_button_size)
|
||||
.with_tooltip_alignment(TooltipAlignment::Left)
|
||||
.with_adjoined_side(AdjoinedSide::Right)
|
||||
@@ -1396,10 +1396,10 @@ impl AgentInputFooter {
|
||||
.cli_agent(ctx)
|
||||
.and_then(plugin_manager_for)
|
||||
.map(|m| m.install_success_message())
|
||||
.unwrap_or("Warp plugin installed. Please restart the session to activate.");
|
||||
.unwrap_or("Galaxy plugin installed. Please restart the session to activate.");
|
||||
self.handle_plugin_operation(
|
||||
"Installing Warp plugin...",
|
||||
"Failed to install Warp plugin",
|
||||
"Installing Galaxy plugin...",
|
||||
"Failed to install Galaxy plugin",
|
||||
success_msg,
|
||||
PluginChipTelemetryKind::Install,
|
||||
|manager| async move { manager.install().await },
|
||||
@@ -1413,10 +1413,10 @@ impl AgentInputFooter {
|
||||
.cli_agent(ctx)
|
||||
.and_then(plugin_manager_for)
|
||||
.map(|m| m.update_success_message())
|
||||
.unwrap_or("Warp plugin updated. Please restart the session to activate.");
|
||||
.unwrap_or("Galaxy plugin updated. Please restart the session to activate.");
|
||||
self.handle_plugin_operation(
|
||||
"Updating Warp plugin...",
|
||||
"Failed to update Warp plugin",
|
||||
"Updating Galaxy plugin...",
|
||||
"Failed to update Galaxy plugin",
|
||||
success_msg,
|
||||
PluginChipTelemetryKind::Update,
|
||||
|manager| async move { manager.update().await },
|
||||
@@ -2823,7 +2823,7 @@ impl ActionButtonTheme for ActiveMicButtonTheme {
|
||||
}
|
||||
}
|
||||
|
||||
/// Green-accented theme for the "Install Warp plugin" chip.
|
||||
/// Green-accented theme for the "Install Galaxy plugin" chip.
|
||||
struct InstallPluginButtonTheme;
|
||||
|
||||
impl ActionButtonTheme for InstallPluginButtonTheme {
|
||||
@@ -2863,7 +2863,7 @@ async fn write_install_log(agent: CLIAgent, err: &PluginInstallError) -> Option<
|
||||
let log_path = env::temp_dir().join("warp-plugin-install.log");
|
||||
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
|
||||
let contents = format!(
|
||||
"Warp plugin installation — {agent:?}\n\
|
||||
"Galaxy plugin installation — {agent:?}\n\
|
||||
{now}\n\
|
||||
\n\
|
||||
{log}",
|
||||
|
||||
@@ -129,7 +129,7 @@ impl AgentToolbarItemKind {
|
||||
Self::NLDToggle => "Autodetection",
|
||||
Self::VoiceInput => "Voice Input",
|
||||
Self::FileAttach => "Attach File",
|
||||
Self::ContextWindowUsage => "Warp Context Indicator",
|
||||
Self::ContextWindowUsage => "Galaxy Context Indicator",
|
||||
Self::LLMContextUsage => "LLM Context Usage",
|
||||
Self::LLMCacheDetails => "LLM Cache Details",
|
||||
Self::FileExplorer => "File Explorer",
|
||||
|
||||
@@ -107,7 +107,7 @@ pub const WAITING_FOR_USER_INPUT_MESSAGE: &str = "Agent waiting for instructions
|
||||
const IMAGE_SOURCE_LINK_LINE_INDEX: usize = 1;
|
||||
|
||||
const ERROR_APOLOGY_TEXT: &str = "I'm sorry, I couldn't complete that request.";
|
||||
const INTERNAL_WARP_ERROR: &str = "Internal Warp error.";
|
||||
const INTERNAL_WARP_ERROR: &str = "Internal Galaxy error.";
|
||||
|
||||
const GALAXY_STATUS_MESSAGES: &[&str] = &[
|
||||
"Warping through the Galaxy...",
|
||||
@@ -3133,7 +3133,7 @@ pub fn render_failed_output(props: FailedOutputProps, app: &AppContext) -> Box<d
|
||||
|
||||
// While an automatic retry/resume is still in flight, don't surface the underlying
|
||||
// transport failure at all. These are typically transient and recover on their own,
|
||||
// so showing the alarming "Warp lost connection" banner (plus debug info) for every
|
||||
// so showing the alarming "Galaxy lost connection" banner (plus debug info) for every
|
||||
// blip is noisy and misleading. Render nothing during in-flight recovery; the full
|
||||
// error banner is only shown once recovery has actually failed. Dogfood builds
|
||||
// (Local/Dev) opt out so developers still see every transport failure aggressively.
|
||||
@@ -3160,7 +3160,7 @@ pub fn render_failed_output(props: FailedOutputProps, app: &AppContext) -> Box<d
|
||||
}
|
||||
}
|
||||
RenderableAIError::ServerOverloaded => {
|
||||
"Warp is currently overloaded. Please try again later.".to_string()
|
||||
"The AI service is currently overloaded. Please try again later.".to_string()
|
||||
}
|
||||
RenderableAIError::InternalWarpError => {
|
||||
format!("{ERROR_APOLOGY_TEXT}\n\n{INTERNAL_WARP_ERROR}")
|
||||
|
||||
@@ -226,6 +226,22 @@ impl AIConversationMetadata {
|
||||
}
|
||||
|
||||
impl BlocklistAIHistoryModel {
|
||||
/// Restore saved local sessions without falling back to a hosted transcript.
|
||||
pub fn load_local_conversation_data(
|
||||
&self,
|
||||
conversation_id: AIConversationId,
|
||||
) -> galaxyui::r#async::BoxFuture<'static, Option<CloudConversationData>> {
|
||||
let conversation = self
|
||||
.load_conversation_from_db(&conversation_id)
|
||||
.filter(|conversation| {
|
||||
!conversation
|
||||
.server_metadata()
|
||||
.is_some_and(|metadata| metadata.ambient_agent_task_id.is_some())
|
||||
})
|
||||
.map(|conversation| CloudConversationData::Oz(Box::new(conversation)));
|
||||
box_future(futures::future::ready(conversation))
|
||||
}
|
||||
|
||||
/// Loads conversation data from the appropriate source (DB or server).
|
||||
///
|
||||
/// This method automatically determines whether to load from the local database or
|
||||
|
||||
@@ -877,7 +877,7 @@ impl RequestedCommandView {
|
||||
) if show_for_action_id == &self.action_id => {
|
||||
*shown.lock() = true;
|
||||
Some(render_autonomy_checkbox_setting_speedbump_footer(
|
||||
"Always allow Oz to execute read-only commands (relies on model)",
|
||||
"Always allow Galaxy to execute read-only commands (relies on model)",
|
||||
*checked,
|
||||
AIBlockAction::ToggleAutoexecuteReadonlyCommandsSpeedbumpCheckbox,
|
||||
self.autoexecute_readonly_commands_speedbump_checkbox_handle
|
||||
|
||||
@@ -246,9 +246,20 @@ pub struct ConversationDetailsData {
|
||||
harness: Option<Harness>,
|
||||
/// Error details displayed when the API call to fetch run data failed.
|
||||
fetch_error: Option<TaskFetchError>,
|
||||
/// Local history resumes the saved session instead of offering a hosted fork.
|
||||
local_history: bool,
|
||||
}
|
||||
|
||||
impl ConversationDetailsData {
|
||||
pub fn for_local_history(mut self) -> Self {
|
||||
self.local_history = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_local_history(&self) -> bool {
|
||||
self.local_history
|
||||
}
|
||||
|
||||
fn directory_for_task(task: &AmbientAgentTask, app: &AppContext) -> Option<String> {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let conversation_id = history_model
|
||||
@@ -358,6 +369,7 @@ impl ConversationDetailsData {
|
||||
skill_spec: None,
|
||||
harness,
|
||||
fetch_error: None,
|
||||
local_history: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +434,7 @@ impl ConversationDetailsData {
|
||||
skill_spec,
|
||||
harness,
|
||||
fetch_error: None,
|
||||
local_history: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +507,7 @@ impl ConversationDetailsData {
|
||||
skill_spec,
|
||||
harness,
|
||||
fetch_error: None,
|
||||
local_history: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -521,6 +535,7 @@ impl ConversationDetailsData {
|
||||
skill_spec: None,
|
||||
harness,
|
||||
fetch_error: None,
|
||||
local_history: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,6 +567,7 @@ impl ConversationDetailsData {
|
||||
skill_spec: None,
|
||||
harness: None,
|
||||
fetch_error,
|
||||
local_history: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +610,7 @@ impl ConversationDetailsData {
|
||||
skill_spec: None,
|
||||
harness,
|
||||
fetch_error: None,
|
||||
local_history: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -752,6 +769,10 @@ impl ConversationDetailsPanel {
|
||||
&self,
|
||||
app: &AppContext,
|
||||
) -> Option<DetailsPanelLocalContinuationInfo> {
|
||||
if self.data.is_local_history() {
|
||||
// History's Open action restores the original local session directly.
|
||||
return None;
|
||||
}
|
||||
if !AISettings::as_ref(app).is_any_ai_enabled(app) {
|
||||
return None;
|
||||
}
|
||||
@@ -894,6 +915,9 @@ impl ConversationDetailsPanel {
|
||||
open_action,
|
||||
data.copy_link_url.clone(),
|
||||
);
|
||||
if data.is_local_history() {
|
||||
config.fork_conversation_id = None;
|
||||
}
|
||||
if !status.as_ref().is_some_and(ConversationStatus::is_done) {
|
||||
config.delete_conversation_id = None;
|
||||
}
|
||||
|
||||
@@ -687,7 +687,7 @@ impl CustomRouterEditorView {
|
||||
);
|
||||
let rules_copy = FormattedText::new([
|
||||
FormattedTextLine::Line(vec![FormattedTextFragment::plain_text(
|
||||
"Rules are custom prompts that describe when to use a specific model. Warp intelligently matches your tasks against these rules.",
|
||||
"Rules are custom prompts that describe when to use a specific model. Galaxy matches your tasks against these rules.",
|
||||
)]),
|
||||
FormattedTextLine::Line(vec![FormattedTextFragment::plain_text(
|
||||
"Rules are matched top to bottom — rules higher in the list take precedence over those below.",
|
||||
@@ -750,7 +750,7 @@ impl CustomRouterEditorView {
|
||||
FormattedTextLine::Line(vec![
|
||||
FormattedTextFragment::bold("Complexity-based"),
|
||||
FormattedTextFragment::plain_text(
|
||||
" routing chooses a model based on Warp's classification of the task's difficulty.",
|
||||
" routing chooses a model based on Galaxy's classification of the task's difficulty.",
|
||||
),
|
||||
]),
|
||||
FormattedTextLine::Line(vec![
|
||||
|
||||
@@ -618,12 +618,12 @@ impl AIDocumentModel {
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
format!("Plan document {ai_document_id} was not found in Warp Drive.")
|
||||
format!("Plan document {ai_document_id} was not found in Galaxy Drive.")
|
||||
})?;
|
||||
let (sync_id, title, content) = notebook;
|
||||
if sync_id.into_server().is_none() {
|
||||
return Err(format!(
|
||||
"Plan document {ai_document_id} is not backed by a saved Warp Drive notebook."
|
||||
"Plan document {ai_document_id} is not backed by a saved Galaxy Drive notebook."
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ pub fn render_models_section(
|
||||
.with_child(render_filterable_dropdown_row(
|
||||
appearance,
|
||||
"Base model",
|
||||
"This model serves as the primary engine behind the agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.",
|
||||
"This model serves as the primary engine behind the agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Galaxy may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.",
|
||||
&view.base_model_dropdown,
|
||||
));
|
||||
|
||||
|
||||
@@ -1729,7 +1729,7 @@ impl View for CodeFooterView {
|
||||
Self::render_status_text(
|
||||
theme,
|
||||
appearance,
|
||||
"Use Oz to update this config".to_string(),
|
||||
"Use Galaxy Agent to update this config".to_string(),
|
||||
),
|
||||
)
|
||||
.finish(),
|
||||
|
||||
@@ -1704,7 +1704,7 @@ impl WorkflowModal {
|
||||
.finish();
|
||||
|
||||
let button_with_tool_tip = appearance.ui_builder().tool_tip_on_element(
|
||||
"Generate a title, descriptions, or parameters with Warp AI".to_string(),
|
||||
"Generate a title, descriptions, or parameters with Galaxy Agent".to_string(),
|
||||
self.button_mouse_states.ai_assist_tool_tip.clone(),
|
||||
rendered_button,
|
||||
ParentAnchor::BottomMiddle,
|
||||
|
||||
@@ -84,15 +84,15 @@ pub(super) fn validate_local_harness_shell(shell_type: Option<ShellType>) -> Res
|
||||
}
|
||||
}
|
||||
|
||||
const LOCAL_CLAUDE_CHILD_ORCHESTRATION_INSTRUCTIONS: &str = r#"You are a local Claude Code child agent launched by a lead agent in Warp.
|
||||
const LOCAL_CLAUDE_CHILD_ORCHESTRATION_INSTRUCTIONS: &str = r#"You are a local Claude Code child agent launched by a lead agent in Galaxy.
|
||||
|
||||
Coordinate with the lead agent through the Oz CLI messaging environment:
|
||||
Coordinate with the lead agent through Galaxy's local CLI messaging environment:
|
||||
- Your run id is in OZ_RUN_ID.
|
||||
- The lead agent id is in OZ_PARENT_RUN_ID.
|
||||
- The Oz CLI command is in OZ_CLI.
|
||||
- The Galaxy CLI command is in OZ_CLI (the compatibility environment variable name).
|
||||
|
||||
If OZ_CLI, OZ_RUN_ID, or OZ_PARENT_RUN_ID is missing, report that blocker in your final response.
|
||||
Do not use Claude Code Agent or SendMessage tools to contact the lead agent; use the Oz CLI commands below.
|
||||
Do not use Claude Code Agent or SendMessage tools to contact the lead agent; use the Galaxy CLI commands below.
|
||||
Do not ask to inspect help before messaging. The command shapes below are complete.
|
||||
|
||||
Send a message to the lead agent at start, when blocked, and when complete:
|
||||
|
||||
@@ -53,7 +53,7 @@ const MODAL_CONTENT_FONT_SIZE: f32 = 14.;
|
||||
const CHECKBOX_SIZE: f32 = 16.;
|
||||
|
||||
const MODAL_TITLE: &str = "Edit prompt";
|
||||
const WARP_PROMPT_SECTION_HEADER: &str = "Warp terminal prompt";
|
||||
const WARP_PROMPT_SECTION_HEADER: &str = "Galaxy terminal prompt";
|
||||
const SHELL_PROMPT_SECTION_HEADER: &str = "Shell prompt (PS1)";
|
||||
const RESTORE_DEFAULT_BUTTON: &str = "Restore default";
|
||||
|
||||
|
||||
@@ -853,6 +853,7 @@ impl View {
|
||||
conversation_id,
|
||||
terminal_view_id,
|
||||
restore_layout: None,
|
||||
local_only: false,
|
||||
});
|
||||
send_telemetry_from_app_ctx!(TelemetryEvent::SelectNavigationPaletteItem, ctx);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,9 @@ use crate::ui_components::icons::Icon as UIIcon;
|
||||
use crate::util::color::{ContrastingColor, MinimumAllowedContrast};
|
||||
use crate::workflows::{AIWorkflowOrigin, WorkflowSource, WorkflowType};
|
||||
|
||||
const OPEN_WARP_AI_ITEM_BODY_TEXT: &str = "Ask Warp AI for command suggestions";
|
||||
const TRANSLATE_WITH_WARP_AI_ITEM_BODY_TEXT: &str = "Translate into shell command using Warp AI";
|
||||
const OPEN_WARP_AI_ITEM_BODY_TEXT: &str = "Ask Galaxy Agent for command suggestions";
|
||||
const TRANSLATE_WITH_WARP_AI_ITEM_BODY_TEXT: &str =
|
||||
"Translate into a shell command using Galaxy Agent";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum WarpAISearchItem {
|
||||
@@ -133,7 +134,7 @@ impl SearchItem for WarpAISearchItem {
|
||||
}
|
||||
|
||||
fn accessibility_label(&self) -> String {
|
||||
format!("Warp AI: {}", self.item_body_text())
|
||||
format!("Galaxy Agent: {}", self.item_body_text())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ pub const CREATE_DOCKER_SANDBOX: StaticCommand = StaticCommand {
|
||||
|
||||
pub static CREATE_NEW_PROJECT: LazyLock<StaticCommand> = LazyLock::new(|| StaticCommand {
|
||||
name: "/create-new-project",
|
||||
description: "Have Oz walk you through creating a new coding project",
|
||||
description: "Have Galaxy walk you through creating a new coding project",
|
||||
icon_path: "bundled/svg/plus.svg",
|
||||
availability: Availability::LOCAL | Availability::AI_ENABLED,
|
||||
auto_enter_ai_mode: true,
|
||||
@@ -79,7 +79,7 @@ pub static CREATE_NEW_PROJECT: LazyLock<StaticCommand> = LazyLock::new(|| Static
|
||||
|
||||
pub static EDIT_SKILL: LazyLock<StaticCommand> = LazyLock::new(|| StaticCommand {
|
||||
name: "/open-skill",
|
||||
description: "Open a skill's markdown file in Warp's built-in editor",
|
||||
description: "Open a skill's markdown file in Galaxy's built-in editor",
|
||||
icon_path: "bundled/svg/file-code-02.svg",
|
||||
availability: Availability::AI_ENABLED,
|
||||
auto_enter_ai_mode: false,
|
||||
@@ -119,7 +119,7 @@ pub const ADD_RULE: StaticCommand = StaticCommand {
|
||||
|
||||
pub static EDIT: LazyLock<StaticCommand> = LazyLock::new(|| StaticCommand {
|
||||
name: "/open-file",
|
||||
description: "Open a file in Warp's code editor",
|
||||
description: "Open a file in Galaxy's code editor",
|
||||
icon_path: "bundled/svg/file-code-02.svg",
|
||||
availability: Availability::LOCAL,
|
||||
auto_enter_ai_mode: false,
|
||||
|
||||
@@ -253,7 +253,7 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
||||
|
||||
// Add command palette entry for toggling between Warp and Classic input modes
|
||||
app.register_fixed_bindings(vec![FixedBinding::empty(
|
||||
"Toggle Input Mode (Warp/Classic)".to_string(),
|
||||
"Toggle Input Mode (Galaxy/Classic)".to_string(),
|
||||
builder(SettingsAction::AppearancePageToggle(
|
||||
AppearancePageAction::ToggleInputMode,
|
||||
)),
|
||||
@@ -1643,7 +1643,7 @@ impl AppearanceSettingsPageView {
|
||||
|
||||
fn input_mode_dropdown_item_label(val: InputMode) -> &'static str {
|
||||
match val {
|
||||
InputMode::PinnedToBottom => "Pin to the bottom (Warp mode)",
|
||||
InputMode::PinnedToBottom => "Pin to the bottom (Galaxy mode)",
|
||||
InputMode::PinnedToTop => "Pin to the top (Reverse mode)",
|
||||
InputMode::Waterfall => "Start at the top (Classic mode)",
|
||||
}
|
||||
@@ -3061,7 +3061,7 @@ impl SettingsWidget for CustomAppIconWidget {
|
||||
);
|
||||
|
||||
let show_dock_icon_toggle = render_body_item::<AppearancePageAction>(
|
||||
"Show Warp in Dock".into(),
|
||||
"Show Galaxy in Dock".into(),
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
ShowDockIconState::storage_key(),
|
||||
@@ -3101,7 +3101,7 @@ impl SettingsWidget for CustomAppIconWidget {
|
||||
appearance
|
||||
.ui_builder()
|
||||
.wrappable_text(
|
||||
"You may need to restart Warp for MacOS to apply the preferred icon style.",
|
||||
"You may need to restart Galaxy for macOS to apply the preferred icon style.",
|
||||
true,
|
||||
)
|
||||
.with_style(UiComponentStyles {
|
||||
@@ -3590,7 +3590,7 @@ impl SettingsWidget for InputTypeWidget {
|
||||
.radio_buttons(
|
||||
self.radio_buttons_states.clone(),
|
||||
vec![
|
||||
RadioButtonItem::text("Warp"),
|
||||
RadioButtonItem::text("Galaxy"),
|
||||
RadioButtonItem::text("Shell (PS1)"),
|
||||
],
|
||||
view.input_type_radio_state.clone(),
|
||||
@@ -5093,7 +5093,7 @@ impl SettingsWidget for UseLatestUserPromptAsConversationTitleInTabNamesWidget {
|
||||
})
|
||||
.finish(),
|
||||
Some(
|
||||
"Show the latest user prompt instead of the generated conversation title for Oz and third-party agent sessions in vertical tabs."
|
||||
"Show the latest user prompt instead of the generated conversation title for Galaxy and third-party agent sessions in vertical tabs."
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -303,7 +303,7 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
||||
];
|
||||
|
||||
toggle_binding_pairs.push(ToggleSettingActionPair::new(
|
||||
"reuse existing SSH ControlMaster in the Warp SSH wrapper",
|
||||
"reuse existing SSH ControlMaster in the Galaxy SSH wrapper",
|
||||
builder(SettingsAction::FeaturesPageToggle(
|
||||
FeaturesPageAction::ToggleSshReuseControlMaster,
|
||||
)),
|
||||
@@ -714,7 +714,7 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
||||
|
||||
if DefaultTerminal::can_warp_become_default() {
|
||||
app.register_fixed_bindings([FixedBinding::empty(
|
||||
"Make Warp the default terminal",
|
||||
"Make Galaxy the default terminal",
|
||||
builder(SettingsAction::FeaturesPageToggle(
|
||||
FeaturesPageAction::MakeWarpDefaultTerminal,
|
||||
)),
|
||||
@@ -4822,9 +4822,9 @@ impl SettingsWidget for LoginItemWidget {
|
||||
let general_settings = GeneralSettings::as_ref(app);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
#[cfg(target_os = "macos")]
|
||||
let label = "Start Warp at login (requires macOS 13+)";
|
||||
let label = "Start Galaxy at login (requires macOS 13+)";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let label = "Start Warp at login";
|
||||
let label = "Start Galaxy at login";
|
||||
render_body_item::<FeaturesPageAction>(
|
||||
label.into(),
|
||||
None,
|
||||
@@ -5101,7 +5101,7 @@ impl SettingsWidget for DefaultTerminalWidget {
|
||||
let default_terminal = DefaultTerminal::as_ref(app);
|
||||
if default_terminal.is_warp_default() {
|
||||
ui_builder
|
||||
.wrappable_text("Warp is the default terminal", true)
|
||||
.wrappable_text("Galaxy is the default terminal", true)
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(appearance.theme().disabled_ui_text_color().into()),
|
||||
margin: Some(Coords::default().bottom(16.)),
|
||||
@@ -5112,7 +5112,7 @@ impl SettingsWidget for DefaultTerminalWidget {
|
||||
} else {
|
||||
ui_builder
|
||||
.link(
|
||||
"Make Warp the default terminal".to_string(),
|
||||
"Make Galaxy the default terminal".to_string(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(FeaturesPageAction::MakeWarpDefaultTerminal);
|
||||
@@ -5207,7 +5207,7 @@ impl SettingsWidget for DesktopNotificationsWidget {
|
||||
let ui_builder = appearance.ui_builder();
|
||||
let mut column = Flex::column();
|
||||
column.add_child(render_body_item::<FeaturesPageAction>(
|
||||
"Receive desktop notifications from Warp".into(),
|
||||
"Receive desktop notifications from Galaxy".into(),
|
||||
Some(AdditionalInfo {
|
||||
mouse_state: self.additional_info_link.clone(),
|
||||
on_click_action: Some(FeaturesPageAction::OpenUrl(NOTIFICATIONS_DOCS_URL.into())),
|
||||
@@ -7552,7 +7552,7 @@ impl SettingsWidget for WindowSystemWidget {
|
||||
may be blurry if your Wayland compositor is using fraction scaling (ex: 125%)."
|
||||
.to_string();
|
||||
if view.force_x11_changed {
|
||||
secondary_text.push_str("\n\nRestart Warp for changes to take effect.");
|
||||
secondary_text.push_str("\n\nRestart Galaxy for changes to take effect.");
|
||||
}
|
||||
let warp_theme = appearance.theme();
|
||||
children.add_child(
|
||||
|
||||
@@ -35,10 +35,10 @@ const OPEN_BUTTON_HEIGHT: f32 = 32.;
|
||||
const ALERT_ACTION_BUTTON_HEIGHT: f32 = 24.;
|
||||
/// Size of the leading icons (search-sm, code-02, alert-circle, oz).
|
||||
const FOOTER_ICON_SIZE: f32 = 16.;
|
||||
/// Size of the Oz brand mark inside the "Fix with Oz" button. Matches the
|
||||
/// Size of the agent icon inside the "Fix with Galaxy Agent" button. Matches the
|
||||
/// Figma spec and the workspace banner's secondary-button icon sizing.
|
||||
const ALERT_OZ_ICON_SIZE: f32 = 14.;
|
||||
/// Horizontal padding inside the "Open file" / "Fix with Oz" action buttons.
|
||||
/// Horizontal padding inside the "Open file" / "Fix with Galaxy Agent" action buttons.
|
||||
/// Matches the workspace banner's secondary button pad.
|
||||
const ALERT_BUTTON_HORIZONTAL_PADDING: f32 = 8.;
|
||||
/// Spacing between the two action buttons when they fit on one row.
|
||||
@@ -250,7 +250,7 @@ pub fn render_settings_error_alert(
|
||||
ui_font_family,
|
||||
text_color,
|
||||
mouse_states.alert_fix_with_oz_button.clone(),
|
||||
"Fix with Oz",
|
||||
"Fix with Galaxy Agent",
|
||||
Some(Icon::Oz),
|
||||
/*bordered=*/ false,
|
||||
WorkspaceAction::FixSettingsWithOz { error_description },
|
||||
|
||||
@@ -193,11 +193,11 @@ impl CliAgentPluginManager for CodexPluginManager {
|
||||
}
|
||||
|
||||
fn install_success_message(&self) -> &'static str {
|
||||
"Warp plugin installed. Please restart Codex to activate."
|
||||
"Galaxy plugin installed. Please restart Codex to activate."
|
||||
}
|
||||
|
||||
fn update_success_message(&self) -> &'static str {
|
||||
"Warp plugin updated. Please restart Codex to activate."
|
||||
"Galaxy plugin updated. Please restart Codex to activate."
|
||||
}
|
||||
|
||||
fn install_instructions(&self) -> &'static PluginInstructions {
|
||||
@@ -273,17 +273,17 @@ impl CliAgentPluginManager for CodexPluginManager {
|
||||
|
||||
static PLUGIN_INSTALL_INSTRUCTIONS: LazyLock<PluginInstructions> =
|
||||
LazyLock::new(|| PluginInstructions {
|
||||
title: "Install Warp Plugin for Codex",
|
||||
title: "Install Galaxy notification plugin for Codex",
|
||||
subtitle: "Run the following commands, then restart Codex.",
|
||||
steps: &[
|
||||
PluginInstructionStep {
|
||||
description: "Add the Warp plugin marketplace repository",
|
||||
description: "Add the notification plugin marketplace repository",
|
||||
command: "codex plugin marketplace add warpdotdev/codex-warp",
|
||||
executable: true,
|
||||
link: None,
|
||||
},
|
||||
PluginInstructionStep {
|
||||
description: "Install the Warp plugin",
|
||||
description: "Install the notification plugin",
|
||||
command: "codex plugin add warp@codex-warp",
|
||||
executable: true,
|
||||
link: None,
|
||||
@@ -294,8 +294,8 @@ static PLUGIN_INSTALL_INSTRUCTIONS: LazyLock<PluginInstructions> =
|
||||
|
||||
static NATIVE_INSTALL_INSTRUCTIONS: LazyLock<PluginInstructions> = LazyLock::new(|| {
|
||||
PluginInstructions {
|
||||
title: "Enable Warp Notifications for Codex",
|
||||
subtitle: "Update Codex to the latest version, then enable in-focus notifications so Warp can display them while you work.",
|
||||
title: "Enable Galaxy notifications for Codex",
|
||||
subtitle: "Update Codex to the latest version, then enable in-focus notifications so Galaxy can display them while you work.",
|
||||
steps: &[
|
||||
PluginInstructionStep {
|
||||
description: "Update Codex to the latest version.",
|
||||
@@ -323,7 +323,7 @@ static EMPTY_INSTRUCTIONS: LazyLock<PluginInstructions> = LazyLock::new(|| Plugi
|
||||
|
||||
static PLUGIN_UPDATE_INSTRUCTIONS: LazyLock<PluginInstructions> = LazyLock::new(|| {
|
||||
PluginInstructions {
|
||||
title: "Update Warp Plugin for Codex",
|
||||
title: "Update Galaxy notification plugin for Codex",
|
||||
subtitle: "Run the following commands, then restart Codex.",
|
||||
steps: &[
|
||||
PluginInstructionStep {
|
||||
@@ -333,7 +333,7 @@ static PLUGIN_UPDATE_INSTRUCTIONS: LazyLock<PluginInstructions> = LazyLock::new(
|
||||
link: None,
|
||||
},
|
||||
PluginInstructionStep {
|
||||
description: "Reinstall the Warp plugin",
|
||||
description: "Reinstall the notification plugin",
|
||||
command: "codex plugin add warp@codex-warp",
|
||||
executable: true,
|
||||
link: None,
|
||||
|
||||
@@ -21,7 +21,7 @@ fn can_auto_install_is_false_without_codex_plugin() {
|
||||
fn install_instructions_are_native_without_codex_plugin() {
|
||||
let _guard = FeatureFlag::CodexPlugin.override_enabled(false);
|
||||
let instructions = CodexPluginManager::new(None, None, None).install_instructions();
|
||||
assert_eq!(instructions.title, "Enable Warp Notifications for Codex");
|
||||
assert_eq!(instructions.title, "Enable Galaxy notifications for Codex");
|
||||
assert_eq!(
|
||||
instructions.steps[1].command,
|
||||
"[tui]\nnotification_condition = \"always\""
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::terminal::view::ambient_agent::AmbientAgentViewModel;
|
||||
use crate::workspace::WorkspaceAction;
|
||||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
||||
|
||||
const AUTO_BEDROCK_TOOLTIP: &str = "Warp uses Bedrock when the model Auto selects supports it; otherwise it may use Warp-hosted inference.";
|
||||
const AUTO_BEDROCK_TOOLTIP: &str = "Galaxy uses Bedrock when the model Auto selects supports it. Other models use their configured provider.";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AcceptModel {
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn wormhole_description(
|
||||
|
||||
let description = FormattedText::new(vec![FormattedTextLine::Line(vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Bring Galaxy's features to your remote session: blocks, full text editing, completions, Oz, and more."
|
||||
"Bring Galaxy's features to your remote session: blocks, full text editing, completions, Galaxy Agent, and more."
|
||||
),
|
||||
])]);
|
||||
wormhole::render::build_description_row(description, theme, appearance, hyperlink_index.clone())
|
||||
|
||||
@@ -7801,24 +7801,24 @@ impl TerminalView {
|
||||
.get_pending_action(app)
|
||||
.map(|action| match &action.action {
|
||||
AIAgentActionType::RequestCommandOutput { command, .. } => {
|
||||
format!("Oz needs your permission to run `{command}`")
|
||||
format!("Galaxy needs your permission to run `{command}`")
|
||||
}
|
||||
AIAgentActionType::ReadFiles(..) => {
|
||||
"Oz needs your permission to read files".to_string()
|
||||
"Galaxy needs your permission to read files".to_string()
|
||||
}
|
||||
AIAgentActionType::SearchCodebase(..) => {
|
||||
"Oz needs your permission to search your codebase".to_string()
|
||||
"Galaxy needs your permission to search your codebase".to_string()
|
||||
}
|
||||
AIAgentActionType::RequestFileEdits { .. } => {
|
||||
"Oz needs your permission to edit a file".to_string()
|
||||
"Galaxy needs your permission to edit a file".to_string()
|
||||
}
|
||||
AIAgentActionType::WriteToLongRunningShellCommand { .. } => {
|
||||
"Oz needs your permission to interact with a running shell command"
|
||||
"Galaxy needs your permission to interact with a running shell command"
|
||||
.to_string()
|
||||
}
|
||||
_ => "Oz needs your confirmation to continue".to_string(),
|
||||
_ => "Galaxy needs your confirmation to continue".to_string(),
|
||||
})
|
||||
.unwrap_or("Oz needs your confirmation to continue".to_string());
|
||||
.unwrap_or("Galaxy needs your confirmation to continue".to_string());
|
||||
return Some(AIBlockNotificationSummary {
|
||||
success: false,
|
||||
title,
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::workflows::workflow::Workflow;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref LAUNCH_CONFIG_COMMENT: String = format!(
|
||||
"# Warp Launch Configuration
|
||||
"# Galaxy Launch Configuration
|
||||
#
|
||||
#
|
||||
# Use this to start a certain configuration of windows, tabs, and panes.
|
||||
|
||||
@@ -592,6 +592,8 @@ pub enum WorkspaceAction {
|
||||
/// If provided, use this layout to restore the conversation.
|
||||
/// Otherwise, fall back to the user's setting.
|
||||
restore_layout: Option<RestoreConversationLayout>,
|
||||
/// History recovery must use this device's saved data without server fallback.
|
||||
local_only: bool,
|
||||
},
|
||||
/// Fork an existing AI conversation.
|
||||
/// Optionally summarizes the conversation after forking and/or sends an initial prompt.
|
||||
|
||||
@@ -7,7 +7,7 @@ use command::blocking::Command;
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use galaxy_util::path::ShellFamily;
|
||||
|
||||
/// Compute the target path where the Oz CLI symlink should be installed, based on channel
|
||||
/// Compute the target path where the Galaxy AI CLI symlink should be installed, based on channel
|
||||
fn oz_install_target_path() -> PathBuf {
|
||||
PathBuf::from("/usr/local/bin").join(ChannelState::channel().cli_command_name())
|
||||
}
|
||||
@@ -19,8 +19,8 @@ fn galaxyctrl_install_target_path() -> PathBuf {
|
||||
|
||||
/// Compute the source path of the galaxyctrl wrapper inside the current app bundle.
|
||||
///
|
||||
/// Oz commands are part of the shared executable's normal argument parser, so
|
||||
/// Oz can symlink directly to the current executable. Galaxy Control has a
|
||||
/// Galaxy AI commands are part of the shared executable's normal argument parser, so
|
||||
/// Galaxy AI can symlink directly to the current executable. Galaxy Control has a
|
||||
/// separate parser selected by the hidden `--galaxyctrl` flag, so its installed
|
||||
/// symlink must target the bundled wrapper that injects that flag. Without it,
|
||||
/// Galaxy Control subcommands such as `tab` would reach the normal parser and be
|
||||
@@ -196,21 +196,21 @@ fn uninstall_symlink(target: &Path, command_name: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install the Oz CLI by symlinking the shared Warp executable into /usr/local/bin.
|
||||
/// Install the Galaxy AI CLI by symlinking the shared Galaxy executable into /usr/local/bin.
|
||||
///
|
||||
/// The normal argument parser dispatches Oz subcommands directly. It also uses
|
||||
/// the `oz`-prefixed invocation name to print CLI help rather than launch the
|
||||
/// The normal argument parser dispatches Galaxy AI subcommands directly. It also
|
||||
/// recognizes the CLI invocation name to print CLI help rather than launch the
|
||||
/// GUI when no subcommand is provided.
|
||||
pub fn install_oz() -> Result<()> {
|
||||
let oz_path = oz_install_target_path();
|
||||
let current_binary =
|
||||
std::env::current_exe().context("Failed to get current executable path")?;
|
||||
install_symlink(¤t_binary, &oz_path, "Oz CLI")
|
||||
install_symlink(¤t_binary, &oz_path, "Galaxy AI CLI")
|
||||
}
|
||||
|
||||
/// Uninstall the Oz CLI by removing the symlink from /usr/local/bin
|
||||
/// Uninstall the Galaxy AI CLI by removing the symlink from /usr/local/bin
|
||||
pub fn uninstall_oz() -> Result<()> {
|
||||
uninstall_symlink(&oz_install_target_path(), "Oz command")
|
||||
uninstall_symlink(&oz_install_target_path(), "Galaxy AI command")
|
||||
}
|
||||
|
||||
/// Install Galaxy Control by symlinking its bundled wrapper into /usr/local/bin.
|
||||
|
||||
@@ -1234,20 +1234,20 @@ pub fn init(app: &mut AppContext) {
|
||||
.with_context_predicate(id!("Workspace") & id!(flags::ENABLE_WARP_DRIVE))]);
|
||||
}
|
||||
|
||||
// Oz and Galaxy Control CLI install/uninstall actions (macOS only)
|
||||
// Galaxy AI and Galaxy Control CLI install/uninstall actions (macOS only)
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
app.register_editable_bindings([
|
||||
EditableBinding::new(
|
||||
"workspace:install_cli",
|
||||
"Install Oz CLI globally for use outside of Warp",
|
||||
"Install Galaxy AI CLI globally for use outside of Galaxy",
|
||||
WorkspaceAction::InstallOz,
|
||||
)
|
||||
.with_group(bindings::BindingGroup::Settings.as_str())
|
||||
.with_context_predicate(id!("Workspace")),
|
||||
EditableBinding::new(
|
||||
"workspace:uninstall_cli",
|
||||
"Undo global Oz CLI installation (oz will still work within Warp)",
|
||||
"Remove global Galaxy AI CLI installation",
|
||||
WorkspaceAction::UninstallOz,
|
||||
)
|
||||
.with_group(bindings::BindingGroup::Settings.as_str())
|
||||
|
||||
+81
-34
@@ -8950,34 +8950,35 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the Oz CLI by creating a symlink in /usr/local/bin
|
||||
/// Install the Galaxy AI CLI by creating a symlink in /usr/local/bin
|
||||
#[cfg(target_os = "macos")]
|
||||
fn install_oz(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
ctx.spawn(async { cli_install::install_oz() }, |view, result, ctx| {
|
||||
let command_name = ChannelState::channel().cli_command_name();
|
||||
let message = format!("Installed the Oz CLI globally. You can now run '{command_name}' from any terminal outside of Warp.");
|
||||
let toast = DismissibleToast::success(message).with_link(
|
||||
ToastLink::new("Learn more".to_string())
|
||||
.with_href("https://docs.warp.dev/reference/cli".to_string()),
|
||||
let message = format!("Installed the Galaxy AI CLI globally. You can now run '{command_name}' from any terminal.");
|
||||
let toast = DismissibleToast::success(message);
|
||||
view.handle_cli_command_result(
|
||||
result,
|
||||
toast,
|
||||
"Failed to install Galaxy AI command",
|
||||
ctx,
|
||||
);
|
||||
view.handle_cli_command_result(result, toast, "Failed to install Oz command", ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Uninstall the Oz CLI by removing the symlink from /usr/local/bin
|
||||
/// Uninstall the Galaxy AI CLI by removing the symlink from /usr/local/bin
|
||||
#[cfg(target_os = "macos")]
|
||||
fn uninstall_oz(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
ctx.spawn(
|
||||
async { cli_install::uninstall_oz() },
|
||||
|view, result, ctx| {
|
||||
let toast = DismissibleToast::success(
|
||||
"Removed the global Oz CLI installation — it still works inside Warp."
|
||||
.to_string(),
|
||||
"Removed the global Galaxy AI CLI installation.".to_string(),
|
||||
);
|
||||
view.handle_cli_command_result(
|
||||
result,
|
||||
toast,
|
||||
"Failed to uninstall Oz command",
|
||||
"Failed to uninstall Galaxy AI command",
|
||||
ctx,
|
||||
);
|
||||
},
|
||||
@@ -9212,10 +9213,10 @@ impl Workspace {
|
||||
let window_id = self.window_id;
|
||||
let view_id = self.agent_management_view.id();
|
||||
AgentConversationsModel::handle(ctx).update(ctx, |model, ctx| {
|
||||
// This history surface consumes local conversations, not hosted run updates.
|
||||
model.register_view_closed(window_id, view_id, ctx);
|
||||
if is_open {
|
||||
model.register_view_open(window_id, view_id, ctx);
|
||||
} else {
|
||||
model.register_view_closed(window_id, view_id, ctx);
|
||||
model.sync_conversations(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12978,6 +12979,7 @@ impl Workspace {
|
||||
pane_view_locator: Option<PaneViewLocator>,
|
||||
terminal_view_id: Option<EntityId>,
|
||||
mut restore_layout: Option<RestoreConversationLayout>,
|
||||
local_only: bool,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// If we have all required navigation data, try to navigate to the existing pane
|
||||
@@ -13022,8 +13024,27 @@ impl Workspace {
|
||||
UndoCloseStack::handle(ctx).update(ctx, |stack, ctx| {
|
||||
stack.discard_pane_group_parent(pane_view_locator.pane_group_id, ctx);
|
||||
});
|
||||
} else if has_blocking_long_running_command {
|
||||
// Don't open in this pane and use the existing restore layout.
|
||||
} else if has_blocking_long_running_command
|
||||
|| (local_only
|
||||
&& (ctx
|
||||
.view_with_id::<TerminalView>(window_id, terminal_view_id)
|
||||
.is_none()
|
||||
|| ctx
|
||||
.view_with_id::<PaneGroup>(window_id, pane_view_locator.pane_group_id)
|
||||
.and_then(|pane_group| {
|
||||
pane_group.as_ref(ctx).terminal_view_from_pane_id(
|
||||
pane_view_locator.pane_id,
|
||||
ctx,
|
||||
)
|
||||
})
|
||||
.is_none_or(|terminal| terminal.id() != terminal_view_id)
|
||||
|| ctx.root_view_id(window_id).is_none()))
|
||||
|| (local_only
|
||||
&& BlocklistAIHistoryModel::as_ref(ctx)
|
||||
.conversation(&conversation_id)
|
||||
.is_none())
|
||||
{
|
||||
// Local history restores from disk if its previous pane is unavailable.
|
||||
} else {
|
||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
|
||||
history_model.set_active_conversation_id(
|
||||
@@ -13070,13 +13091,13 @@ impl Workspace {
|
||||
// Handle based on effective layout
|
||||
match effective_layout {
|
||||
RestoreConversationLayout::ActivePane => {
|
||||
self.restore_conversation_in_active_pane(conversation_id, ctx);
|
||||
self.restore_conversation_in_active_pane(conversation_id, local_only, ctx);
|
||||
}
|
||||
RestoreConversationLayout::SplitPane => {
|
||||
self.restore_conversation_in_split_pane(conversation_id, ctx);
|
||||
self.restore_conversation_in_split_pane(conversation_id, local_only, ctx);
|
||||
}
|
||||
RestoreConversationLayout::NewTab => {
|
||||
self.restore_conversation_in_new_tab(conversation_id, ctx);
|
||||
self.restore_conversation_in_new_tab(conversation_id, local_only, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13087,6 +13108,7 @@ impl Workspace {
|
||||
fn restore_conversation_in_active_pane(
|
||||
&mut self,
|
||||
conversation_id: AIConversationId,
|
||||
local_only: bool,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let terminal_view_for_active_pane = self.active_session_view(ctx).filter(|_| {
|
||||
@@ -13097,7 +13119,7 @@ impl Workspace {
|
||||
|
||||
// If we can't restore in the active pane, fall back to restoring in new tab.
|
||||
let Some(terminal_view) = &terminal_view_for_active_pane else {
|
||||
self.restore_conversation_in_new_tab(conversation_id, ctx);
|
||||
self.restore_conversation_in_new_tab(conversation_id, local_only, ctx);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -13143,7 +13165,7 @@ impl Workspace {
|
||||
});
|
||||
// If there's an active long-running command in the active terminal, fall back to restoring in new tab.
|
||||
if active_pane_has_long_running_command {
|
||||
self.restore_conversation_in_new_tab(conversation_id, ctx);
|
||||
self.restore_conversation_in_new_tab(conversation_id, local_only, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13152,9 +13174,11 @@ impl Workspace {
|
||||
.as_ref(ctx)
|
||||
.get_conversation_metadata(&conversation_id)
|
||||
.is_some_and(|m| m.has_local_data);
|
||||
let future = history_model
|
||||
.as_ref(ctx)
|
||||
.load_conversation_data(conversation_id, ctx);
|
||||
let future = if local_only {
|
||||
history_model.as_ref(ctx).load_local_conversation_data(conversation_id)
|
||||
} else {
|
||||
history_model.as_ref(ctx).load_conversation_data(conversation_id, ctx)
|
||||
};
|
||||
let terminal_view_for_closure = terminal_view.clone();
|
||||
let window_id = ctx.window_id();
|
||||
ctx.spawn(future, move |_workspace, conversation, ctx| {
|
||||
@@ -13169,7 +13193,12 @@ impl Workspace {
|
||||
ctx.notify();
|
||||
});
|
||||
WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
let toast = DismissibleToast::error("Failed to load conversation.".to_owned());
|
||||
let message = if local_only {
|
||||
"This AI session is not available on this device."
|
||||
} else {
|
||||
"Failed to load conversation."
|
||||
};
|
||||
let toast = DismissibleToast::error(message.to_owned());
|
||||
toast_stack.add_ephemeral_toast(toast, window_id, ctx);
|
||||
});
|
||||
return;
|
||||
@@ -13208,6 +13237,7 @@ impl Workspace {
|
||||
fn restore_conversation_in_split_pane(
|
||||
&mut self,
|
||||
conversation_id: AIConversationId,
|
||||
local_only: bool,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let window_id = ctx.window_id();
|
||||
@@ -13223,14 +13253,21 @@ impl Workspace {
|
||||
});
|
||||
|
||||
let history_model = BlocklistAIHistoryModel::handle(ctx);
|
||||
let future = history_model
|
||||
.as_ref(ctx)
|
||||
.load_conversation_data(conversation_id, ctx);
|
||||
let future = if local_only {
|
||||
history_model.as_ref(ctx).load_local_conversation_data(conversation_id)
|
||||
} else {
|
||||
history_model.as_ref(ctx).load_conversation_data(conversation_id, ctx)
|
||||
};
|
||||
ctx.spawn(future, move |_workspace, conversation, ctx| {
|
||||
let Some(conversation) = conversation else {
|
||||
log::warn!("Failed to load conversation {conversation_id}");
|
||||
WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
let toast = DismissibleToast::error("Failed to load conversation.".to_owned());
|
||||
let message = if local_only {
|
||||
"This AI session is not available on this device."
|
||||
} else {
|
||||
"Failed to load conversation."
|
||||
};
|
||||
let toast = DismissibleToast::error(message.to_owned());
|
||||
toast_stack.add_ephemeral_toast(toast, window_id, ctx);
|
||||
});
|
||||
// Close the loading pane
|
||||
@@ -13262,6 +13299,7 @@ impl Workspace {
|
||||
fn restore_conversation_in_new_tab(
|
||||
&mut self,
|
||||
conversation_id: AIConversationId,
|
||||
local_only: bool,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let window_id = ctx.window_id();
|
||||
@@ -13291,15 +13329,22 @@ impl Workspace {
|
||||
let loading_pane_id = new_pane_group.as_ref(ctx).focused_pane_id(ctx);
|
||||
|
||||
let history_model = BlocklistAIHistoryModel::handle(ctx);
|
||||
let future = history_model
|
||||
.as_ref(ctx)
|
||||
.load_conversation_data(conversation_id, ctx);
|
||||
let future = if local_only {
|
||||
history_model.as_ref(ctx).load_local_conversation_data(conversation_id)
|
||||
} else {
|
||||
history_model.as_ref(ctx).load_conversation_data(conversation_id, ctx)
|
||||
};
|
||||
|
||||
ctx.spawn(future, move |workspace, conversation, ctx| {
|
||||
let Some(conversation) = conversation else {
|
||||
log::warn!("Failed to load conversation {conversation_id}");
|
||||
WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
let toast = DismissibleToast::error("Failed to load conversation.".to_owned());
|
||||
let message = if local_only {
|
||||
"This AI session is not available on this device."
|
||||
} else {
|
||||
"Failed to load conversation."
|
||||
};
|
||||
let toast = DismissibleToast::error(message.to_owned());
|
||||
toast_stack.add_ephemeral_toast(toast, window_id, ctx);
|
||||
});
|
||||
// Close the loading tab
|
||||
@@ -21892,7 +21937,7 @@ impl Workspace {
|
||||
AISettings::as_ref(app)
|
||||
.is_any_ai_enabled(app)
|
||||
.then(|| WorkspaceBannerButtonDetails {
|
||||
text: "Fix with Oz".to_owned(),
|
||||
text: "Fix with Galaxy Agent".to_owned(),
|
||||
action: WorkspaceAction::FixSettingsWithOz {
|
||||
error_description: error.to_string(),
|
||||
},
|
||||
@@ -24893,7 +24938,7 @@ impl TypedActionView for Workspace {
|
||||
}
|
||||
RunAISuggestedCommand(code) => {
|
||||
let command = code.trim().to_string();
|
||||
let workflow = Workflow::new("Command from Oz", command);
|
||||
let workflow = Workflow::new("Command from Galaxy Agent", command);
|
||||
self.run_workflow_in_active_input(
|
||||
&WorkflowType::AIGenerated {
|
||||
workflow,
|
||||
@@ -25148,6 +25193,7 @@ impl TypedActionView for Workspace {
|
||||
conversation_id,
|
||||
terminal_view_id,
|
||||
restore_layout,
|
||||
local_only,
|
||||
} => {
|
||||
self.restore_or_navigate_to_conversation(
|
||||
*conversation_id,
|
||||
@@ -25155,6 +25201,7 @@ impl TypedActionView for Workspace {
|
||||
*pane_view_locator,
|
||||
*terminal_view_id,
|
||||
*restore_layout,
|
||||
*local_only,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ use super::view_model::{ConversationEntry, ConversationListViewModel};
|
||||
use crate::ai::active_agent_views_model::{ActiveAgentViewsModel, ConversationOrTaskId};
|
||||
use crate::ai::agent::conversation::AIConversationId;
|
||||
use crate::ai::agent_conversations_model::{
|
||||
AgentConversationEntryId, AgentConversationNavigationSubject, AgentConversationsModel,
|
||||
AgentConversationEntryId, AgentConversationsModel,
|
||||
};
|
||||
use crate::ai::agent_management::telemetry::{AgentManagementTelemetryEvent, OpenedFrom};
|
||||
use crate::ai::blocklist::history_model::BlocklistAIHistoryModel;
|
||||
@@ -707,8 +707,8 @@ impl ConversationListView {
|
||||
ctx.emit(Event::NewConversationInNewTab);
|
||||
}
|
||||
ListItem::Conversation { entry, .. } => {
|
||||
if let Some(action) = AgentConversationsModel::resolve_open_action(
|
||||
AgentConversationNavigationSubject::Entry(entry.id),
|
||||
if let Some(action) = AgentConversationsModel::resolve_local_open_action(
|
||||
entry.id,
|
||||
None,
|
||||
ctx,
|
||||
) {
|
||||
@@ -1404,8 +1404,8 @@ impl TypedActionView for ConversationListView {
|
||||
});
|
||||
}
|
||||
ConversationListViewAction::OpenItem { id } => {
|
||||
let Some(action) = AgentConversationsModel::resolve_open_action(
|
||||
AgentConversationNavigationSubject::Entry(*id),
|
||||
let Some(action) = AgentConversationsModel::resolve_local_open_action(
|
||||
*id,
|
||||
None,
|
||||
ctx,
|
||||
) else {
|
||||
|
||||
@@ -77,7 +77,7 @@ impl ConversationListViewModel {
|
||||
fn refresh_cached_items(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
let model = self.conversations_model.as_ref(ctx);
|
||||
self.cached_entry_ids = model
|
||||
.get_entries(
|
||||
.get_local_entries(
|
||||
&AgentManagementFilters {
|
||||
owners: OwnerFilter::PersonalOnly,
|
||||
status: StatusFilter::All,
|
||||
@@ -127,7 +127,7 @@ impl ConversationListViewModel {
|
||||
.cached_entry_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let item = conversations_model.get_entry_by_id(id, ctx)?;
|
||||
let item = conversations_model.get_local_entry_by_id(id, ctx)?;
|
||||
|
||||
match_indices_case_insensitive(&item.display.title, &search_query).map(
|
||||
|result| {
|
||||
@@ -165,7 +165,7 @@ impl ConversationListViewModel {
|
||||
ctx: &AppContext,
|
||||
) -> Option<AgentConversationEntry> {
|
||||
let model = self.conversations_model.as_ref(ctx);
|
||||
model.get_entry_by_id(id, ctx)
|
||||
model.get_local_entry_by_id(id, ctx)
|
||||
}
|
||||
|
||||
pub fn current_ids(&self) -> impl Iterator<Item = &AgentConversationEntryId> {
|
||||
|
||||
@@ -1089,9 +1089,7 @@ impl LeftPanelView {
|
||||
}
|
||||
}
|
||||
|
||||
/// When the conversation list view's visibility changes,
|
||||
/// we need to update the conversation and tasks model to reflect the new state
|
||||
/// (this information is used to decide whether or not we should poll for new tasks).
|
||||
/// Refresh local history without subscribing this surface to hosted task polling.
|
||||
fn on_conversation_list_view_visibility_changed(
|
||||
&self,
|
||||
is_now_open: bool,
|
||||
@@ -1100,10 +1098,9 @@ impl LeftPanelView {
|
||||
let window_id = ctx.window_id();
|
||||
let view_id = self.conversation_list_view.id();
|
||||
AgentConversationsModel::handle(ctx).update(ctx, |model, ctx| {
|
||||
model.register_view_closed(window_id, view_id, ctx);
|
||||
if is_now_open {
|
||||
model.register_view_open(window_id, view_id, ctx);
|
||||
} else {
|
||||
model.register_view_closed(window_id, view_id, ctx);
|
||||
model.sync_conversations(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4182,7 +4182,7 @@ fn terminal_kind_badge_label(is_oz_agent: bool, cli_agent: Option<CLIAgent>) ->
|
||||
if let Some(cli_agent) = cli_agent {
|
||||
cli_agent.display_name().to_string()
|
||||
} else if is_oz_agent {
|
||||
"Oz".to_string()
|
||||
"Galaxy Agent".to_string()
|
||||
} else {
|
||||
"Terminal".to_string()
|
||||
}
|
||||
|
||||
@@ -928,7 +928,7 @@ fn restore_conversation_in_active_pane_enters_existing_live_conversation_without
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
assert_eq!(workspace.tab_count(), 1);
|
||||
|
||||
workspace.restore_conversation_in_active_pane(conversation_id, ctx);
|
||||
workspace.restore_conversation_in_active_pane(conversation_id, false, ctx);
|
||||
|
||||
assert_eq!(workspace.tab_count(), 1);
|
||||
});
|
||||
@@ -3577,7 +3577,7 @@ fn test_standard_tab_context_menu_shows_hover_only_tab_bar() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_cloud_agent_setup_guide_action_opens_management_view_and_is_idempotent() {
|
||||
fn test_legacy_cloud_setup_guide_action_opens_local_history_without_onboarding() {
|
||||
let _agent_management_guard = FeatureFlag::AgentManagementView.override_enabled(true);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
@@ -3598,7 +3598,7 @@ fn test_open_cloud_agent_setup_guide_action_opens_management_view_and_is_idempot
|
||||
.current_workspace_state
|
||||
.is_agent_management_view_open
|
||||
);
|
||||
assert!(workspace
|
||||
assert!(!workspace
|
||||
.agent_management_view
|
||||
.as_ref(ctx)
|
||||
.is_showing_setup_guide());
|
||||
@@ -3609,7 +3609,7 @@ fn test_open_cloud_agent_setup_guide_action_opens_management_view_and_is_idempot
|
||||
.current_workspace_state
|
||||
.is_agent_management_view_open
|
||||
);
|
||||
assert!(workspace
|
||||
assert!(!workspace
|
||||
.agent_management_view
|
||||
.as_ref(ctx)
|
||||
.is_showing_setup_guide());
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Harness {
|
||||
|
||||
pub fn display_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Oz => "Oz",
|
||||
Self::Oz => "Galaxy",
|
||||
Self::Claude => "Claude Code",
|
||||
Self::OpenCode => "OpenCode",
|
||||
Self::Gemini => "Gemini CLI",
|
||||
|
||||
Reference in New Issue
Block a user