Port upstream UI polish and refresh dependencies

Bring over selected Warp UI improvements: selectable Thought text, message
timestamps, animated theme backgrounds, editable New Window shortcuts,
safer tab renaming and restoration, and text/font/discard crash fixes.

Match upstream orchestration pill geometry and tooltips, and replace expanded
subagent previews with compact links that retain live status and conversation
navigation for launches and follow-up messages.

Include the pending Cargo.lock dependency refresh.

Adapted upstream PRs: #15948, #15884, #15691, #14618, #15772, #15705,
#15605, #14895, #15996, #15771, #14893, and #14603.
This commit is contained in:
2026-09-17 23:44:47 -05:00
parent f084548b34
commit a29ee94d84
46 changed files with 2529 additions and 1479 deletions
+29 -1
View File
@@ -530,6 +530,7 @@ use crate::workspace::view::cloud_agent_capacity_modal::CloudAgentCapacityModalV
use crate::workspace::{
CommandSearchOptions, ForkAIConversationParams, ForkFromExchange,
ForkedConversationDestination, OneTimeModalModel, ToastStack, WorkspaceAction,
WorkspaceRegistry,
};
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
use crate::workspaces::workspace::CustomerType;
@@ -1428,6 +1429,9 @@ pub enum ContextMenuAction {
CopyAIBlockQuery {
ai_block_view_id: EntityId,
},
CopyAIBlockTimestamp {
ai_block_view_id: EntityId,
},
/// Copy the AI block output text
CopyAIBlockOutput {
ai_block_view_id: EntityId,
@@ -1528,6 +1532,7 @@ impl fmt::Debug for ContextMenuAction {
StopSharing => f.write_str("StopSharing"),
CopyAIDebuggingLink { .. } => f.write_str("CopyAIDebuggingLink"),
CopyAIBlockQuery { .. } => f.write_str("CopyAIBlockPrompt"),
CopyAIBlockTimestamp { .. } => f.write_str("CopyAIBlockTimestamp"),
CopyAIBlockOutput { .. } => f.write_str("CopyAIBlockOutput"),
CopyAIBlock { .. } => f.write_str("CopyAIBlockBoth"),
CopyAIBlockConversation { .. } => f.write_str("CopyAIBlockConversation"),
@@ -12149,7 +12154,17 @@ impl TerminalView {
// case, we want the block to be focused because otherwise,
// users get stuck as they'd otherwise need to click into the
// box to respond to whether or not they want to update oh my zsh.
self.focus_terminal(ctx);
//
// Skipped while a tab or tab-group rename editor is focused. Taking focus
// would end the rename and lose user inputs (#14241).
let inline_rename_editor_is_focused = WorkspaceRegistry::as_ref(ctx)
.get(self.window_id, ctx)
.is_some_and(|workspace| {
workspace.as_ref(ctx).is_inline_rename_editor_focused(ctx)
});
if !inline_rename_editor_is_focused {
self.focus_terminal(ctx);
}
}
ModelEvent::AfterBlockStarted {
command,
@@ -24998,6 +25013,19 @@ impl TerminalView {
}
}
}
CopyAIBlockTimestamp { ai_block_view_id } => {
for rich_content in self.rich_content_views.iter() {
if let Some(ai_metadata) = rich_content
.ai_block_metadata()
.filter(|metadata| metadata.ai_block_handle.id() == *ai_block_view_id)
{
ai_metadata.ai_block_handle.update(ctx, |block, ctx| {
block.handle_action(&AIBlockAction::CopyTimestamp, ctx);
});
break;
}
}
}
CopyAIBlockOutput { ai_block_view_id } => {
// Copy only the current AI block's output
for rich_content in self.rich_content_views.iter() {
@@ -74,6 +74,8 @@ fn render_user_query(
&user_display_name,
profile_image_url.as_ref(),
None,
None,
None,
&Default::default(),
&Default::default(),
0,
+21
View File
@@ -119,6 +119,27 @@ impl TerminalView {
.into_item(),
);
}
let has_query_timestamp = self.rich_content_views.iter().any(|rich_content| {
rich_content
.ai_block_metadata()
.filter(|metadata| metadata.ai_block_handle.id() == ai_block_view_id)
.is_some_and(|metadata| {
metadata
.ai_block_handle
.as_ref(ctx)
.query_sent_at(ctx)
.is_some()
})
});
if has_query_timestamp {
items.push(
MenuItemFields::new("Copy timestamp")
.with_on_select_action(TerminalAction::ContextMenu(
ContextMenuAction::CopyAIBlockTimestamp { ai_block_view_id },
))
.into_item(),
);
}
items.push(MenuItem::Separator);
items.push(
MenuItemFields::new("Save as prompt")
+73 -1
View File
@@ -51,6 +51,7 @@ use crate::server::ids::{ClientId, SyncId};
use crate::server::server_api::ai::SpawnAgentRequest;
use crate::settings::import::model::ImportedConfigModel;
use crate::settings::{AISettings, AppEditorSettings, WarpPromptSeparator};
use crate::tab::NewSessionMenuItem;
use crate::terminal::alt_screen::should_intercept_mouse;
use crate::terminal::block_list_element::{SnackbarPoint, SnackbarTranslationMode};
use crate::terminal::block_list_viewport::{ClampingMode, ScrollLines};
@@ -84,7 +85,8 @@ use crate::test_util::terminal::{
};
use crate::test_util::{add_window_with_terminal, assert_eventually};
use crate::view_components::find::FindWithinBlockState;
use crate::workspace::ToastStack;
use crate::workspace::view::tests::{initialize_app as initialize_workspace_app, mock_workspace};
use crate::workspace::{ToastStack, WorkspaceAction};
use crate::ActiveAgentViewsModel;
fn add_window_with_cloud_mode_terminal(app: &mut App) -> ViewHandle<TerminalView> {
@@ -8427,3 +8429,73 @@ fn hidden_tool_control_changes_preserve_pinned_and_unpinned_scroll() {
});
});
}
#[test]
fn visible_bootstrap_block_leaves_focus_on_tab_rename_editor() {
App::test((), |mut app| async move {
initialize_workspace_app(&mut app);
let workspace = mock_workspace(&mut app);
let (window, terminal) = workspace.update(&mut app, |workspace, ctx| {
workspace.rename_tab(0, ctx);
let terminal = workspace
.active_tab_pane_group()
.as_ref(ctx)
.active_session_view(ctx)
.expect("tab should contain a terminal");
(ctx.window_id(), terminal)
});
assert!(workspace.read(&app, |workspace, ctx| {
workspace.is_inline_rename_editor_focused(ctx)
}));
let focused_before = app.focused_view_id(window);
terminal.update(&mut app, |view, ctx| {
view.handle_terminal_event(&ModelEvent::VisibleBootstrapBlock, ctx);
});
assert_eq!(app.focused_view_id(window), focused_before);
assert!(workspace.read(&app, |workspace, ctx| {
workspace.is_inline_rename_editor_focused(ctx)
}));
});
}
#[test]
fn visible_bootstrap_block_leaves_focus_on_tab_group_rename_editor() {
let _grouped_tabs_guard = FeatureFlag::GroupedTabs.override_enabled(true);
App::test((), |mut app| async move {
initialize_workspace_app(&mut app);
let workspace = mock_workspace(&mut app);
let (window, terminal, group_id) = workspace.update(&mut app, |workspace, ctx| {
workspace.handle_action(
&WorkspaceAction::SelectNewSessionMenuItem(NewSessionMenuItem::CreateNewTabGroup),
ctx,
);
let group_id = workspace.tabs[0]
.group_id
.expect("active tab should be assigned to the new group");
let terminal = workspace
.active_tab_pane_group()
.as_ref(ctx)
.active_session_view(ctx)
.expect("new tab group should contain a terminal");
(ctx.window_id(), terminal, group_id)
});
workspace.update(&mut app, |workspace, ctx| {
workspace.rename_tab_group(group_id, ctx);
});
assert!(workspace.read(&app, |workspace, ctx| {
workspace.is_inline_rename_editor_focused(ctx)
}));
let focused_before = app.focused_view_id(window);
terminal.update(&mut app, |view, ctx| {
view.handle_terminal_event(&ModelEvent::VisibleBootstrapBlock, ctx);
});
assert_eq!(app.focused_view_id(window), focused_before);
assert!(workspace.read(&app, |workspace, ctx| {
workspace.is_inline_rename_editor_focused(ctx)
}));
});
}