Add sleeping tab recovery

This commit is contained in:
2026-05-08 10:23:58 -04:00
parent 1ec3bfcc3d
commit 3c380e1455
12 changed files with 338 additions and 50 deletions
@@ -21,8 +21,10 @@ mod shortcuts;
mod sidebar_tabs;
mod site_permissions_settings;
mod site_settings;
mod sleep;
mod spaces;
mod sync;
mod tab_context;
mod task_manager;
use ely_browser_core::BrowserSnapshot;
@@ -47,6 +49,9 @@ impl ElyShell {
if tab.state() == &TabState::Crashed {
return self.render_crash_page(tab, snapshot, cx);
}
if tab.state() == &TabState::Discarded {
return self.render_sleep_page(tab, snapshot, cx);
}
match tab.url().as_str() {
"ely://bookmarks" => self.render_bookmarks_page(snapshot, cx),
@@ -3,11 +3,14 @@ use ely_design_system::colors;
use ely_domain::BrowserTab;
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
use gpui_component::{
IconName, Sizable, StyledExt,
IconName, Sizable,
button::{Button, ButtonVariants},
};
use super::{ElyShell, render_canvas_surface};
use super::{
ElyShell, render_canvas_surface,
tab_context::{favicon_status, profile_name, space_name, tab_context_row},
};
impl ElyShell {
pub(super) fn render_crash_page(
@@ -60,12 +63,12 @@ fn render_crash_content(
.flex()
.flex_col()
.gap_3()
.child(crash_detail("URL", tab.url().as_str().to_string()))
.child(crash_detail("Title", tab.title().to_string()))
.child(crash_detail("Favicon", favicon_status(tab)))
.child(crash_detail("Space", space_name(snapshot, tab)))
.child(crash_detail("Profile", profile_name(snapshot, tab)))
.child(crash_detail(
.child(tab_context_row("URL", tab.url().as_str().to_string()))
.child(tab_context_row("Title", tab.title().to_string()))
.child(tab_context_row("Favicon", favicon_status(tab)))
.child(tab_context_row("Space", space_name(snapshot, tab)))
.child(tab_context_row("Profile", profile_name(snapshot, tab)))
.child(tab_context_row(
"Form restore prompt",
"Session data remains attached to this tab.".to_string(),
)),
@@ -155,46 +158,6 @@ fn render_missing_crash_route(message: &'static str) -> AnyElement {
)
}
fn crash_detail(label: &'static str, value: String) -> AnyElement {
div()
.flex()
.items_center()
.justify_between()
.gap_4()
.child(div().min_w(px(128.0)).text_xs().text_color(rgb(colors::MUTED)).child(label))
.child(
div()
.min_w_0()
.text_sm()
.font_semibold()
.truncate()
.text_color(rgb(colors::INK))
.child(value),
)
.into_any_element()
}
fn favicon_status(tab: &BrowserTab) -> String {
tab.favicon_key()
.map_or_else(|| "No favicon saved".to_string(), |favicon| format!("Saved: {favicon}"))
}
fn space_name(snapshot: &BrowserSnapshot, tab: &BrowserTab) -> String {
snapshot
.spaces
.iter()
.find(|space| space.id() == tab.space_id())
.map_or_else(|| tab.space_id().as_str().to_string(), |space| space.name().to_string())
}
fn profile_name(snapshot: &BrowserSnapshot, tab: &BrowserTab) -> String {
snapshot
.profiles
.iter()
.find(|profile| profile.id() == tab.profile_id())
.map_or_else(|| tab.profile_id().as_str().to_string(), |profile| profile.name().to_string())
}
fn crash_route_tab_id(url: &str) -> Option<&str> {
let tab_id = url.strip_prefix("ely://crash/")?;
(!tab_id.is_empty() && !tab_id.contains('/')).then_some(tab_id)
@@ -0,0 +1,129 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors;
use ely_domain::BrowserTab;
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
use gpui_component::{
IconName, Sizable,
button::{Button, ButtonVariants},
};
use super::{
ElyShell, render_canvas_surface,
tab_context::{favicon_status, profile_name, space_name, tab_context_row},
};
impl ElyShell {
pub(super) fn render_sleep_page(
&mut self,
tab: &BrowserTab,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
render_canvas_surface(render_sleep_content(tab, snapshot, cx))
}
}
fn render_sleep_content(
tab: &BrowserTab,
snapshot: &BrowserSnapshot,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let tab_id = tab.id().clone();
div()
.size_full()
.p_8()
.flex()
.flex_col()
.gap_5()
.child(render_sleep_header(tab, cx))
.child(
div()
.border_t_1()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.py_4()
.flex()
.flex_col()
.gap_3()
.child(tab_context_row("URL", tab.url().as_str().to_string()))
.child(tab_context_row("Title", tab.title().to_string()))
.child(tab_context_row("Favicon", favicon_status(tab)))
.child(tab_context_row("Space", space_name(snapshot, tab)))
.child(tab_context_row("Profile", profile_name(snapshot, tab)))
.child(tab_context_row(
"Session",
"Page session remains attached to this tab.".to_string(),
)),
)
.child(
div()
.flex()
.items_center()
.gap_3()
.text_sm()
.text_color(rgb(colors::MUTED))
.child(IconName::CircleCheck)
.child("This tab is sleeping. Restore wakes the tab and keeps its saved context."),
)
.child(
div().flex().child(
Button::new("wake-discarded-tab")
.primary()
.small()
.icon(IconName::Undo2)
.label("Restore")
.tooltip("Restore sleeping tab")
.on_click(cx.listener(move |shell, _, window, cx| {
shell.wake_discarded_tab(&tab_id, window, cx);
})),
),
)
.into_any_element()
}
fn render_sleep_header(tab: &BrowserTab, cx: &mut Context<ElyShell>) -> AnyElement {
let tab_id = tab.id().clone();
div()
.flex()
.items_end()
.justify_between()
.gap_4()
.child(
div()
.min_w_0()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.items_center()
.gap_3()
.text_size(px(26.0))
.text_color(rgb(colors::INK))
.child(IconName::CircleCheck)
.child("Sleeping Tab"),
)
.child(
div()
.text_sm()
.truncate()
.text_color(rgb(colors::MUTED))
.child(format!("Sleeping {}", tab.display_url())),
),
)
.child(
Button::new("wake-discarded-tab-header")
.ghost()
.small()
.icon(IconName::Undo2)
.label("Restore")
.tooltip("Restore sleeping tab")
.on_click(cx.listener(move |shell, _, window, cx| {
shell.wake_discarded_tab(&tab_id, window, cx);
})),
)
.into_any_element()
}
@@ -0,0 +1,45 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors;
use ely_domain::BrowserTab;
use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb};
use gpui_component::StyledExt;
pub(super) fn tab_context_row(label: &'static str, value: String) -> AnyElement {
div()
.flex()
.items_center()
.justify_between()
.gap_4()
.child(div().min_w(px(128.0)).text_xs().text_color(rgb(colors::MUTED)).child(label))
.child(
div()
.min_w_0()
.text_sm()
.font_semibold()
.truncate()
.text_color(rgb(colors::INK))
.child(value),
)
.into_any_element()
}
pub(super) fn favicon_status(tab: &BrowserTab) -> String {
tab.favicon_key()
.map_or_else(|| "No favicon saved".to_string(), |favicon| format!("Saved: {favicon}"))
}
pub(super) fn space_name(snapshot: &BrowserSnapshot, tab: &BrowserTab) -> String {
snapshot
.spaces
.iter()
.find(|space| space.id() == tab.space_id())
.map_or_else(|| tab.space_id().as_str().to_string(), |space| space.name().to_string())
}
pub(super) fn profile_name(snapshot: &BrowserSnapshot, tab: &BrowserTab) -> String {
snapshot
.profiles
.iter()
.find(|profile| profile.id() == tab.profile_id())
.map_or_else(|| tab.profile_id().as_str().to_string(), |profile| profile.name().to_string())
}
+1 -1
View File
@@ -1,6 +1,5 @@
mod archive_labels;
mod bookmarks;
mod crashes;
mod downloads;
mod history;
mod internal_pages;
@@ -13,6 +12,7 @@ mod site_permissions;
mod spaces;
mod splits;
mod tab_groups;
mod tab_lifecycle;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{
@@ -18,4 +18,19 @@ impl ElyShell {
cx.notify();
}
}
pub(super) fn wake_discarded_tab(
&mut self,
tab_id: &TabId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let ShellState::Ready(core) = &mut self.state
&& core.wake_discarded_tab(tab_id).is_ok()
{
self.sync_address_input(window, cx);
self.focus_address_bar(window, cx);
cx.notify();
}
}
}