Add crashed tab recovery

This commit is contained in:
2026-05-08 10:14:00 -04:00
parent f23e5d4105
commit 1ec3bfcc3d
11 changed files with 394 additions and 1 deletions
+21
View File
@@ -0,0 +1,21 @@
use ely_domain::TabId;
use gpui::{Context, Window};
use super::{ElyShell, ShellState};
impl ElyShell {
pub(super) fn recover_crashed_tab(
&mut self,
tab_id: &TabId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let ShellState::Ready(core) = &mut self.state
&& core.recover_crashed_tab(tab_id).is_ok()
{
self.sync_address_input(window, cx);
self.focus_address_bar(window, cx);
cx.notify();
}
}
}
+7 -1
View File
@@ -1,5 +1,6 @@
mod about;
mod bookmarks;
mod crash;
mod download_actions;
mod download_labels;
mod download_settings;
@@ -26,7 +27,7 @@ mod task_manager;
use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing};
use ely_domain::{ArchivedTab, BrowserTab};
use ely_domain::{ArchivedTab, BrowserTab, TabState};
use gpui::{
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
StatefulInteractiveElement, Styled, div, px, rgb,
@@ -43,6 +44,10 @@ impl ElyShell {
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
if tab.state() == &TabState::Crashed {
return self.render_crash_page(tab, snapshot, cx);
}
match tab.url().as_str() {
"ely://bookmarks" => self.render_bookmarks_page(snapshot, cx),
"ely://notes" => self.render_notes_page(snapshot, cx),
@@ -52,6 +57,7 @@ impl ElyShell {
"ely://archive" => self.render_archive_page(snapshot, cx),
"ely://task-manager" => self.render_task_manager_page(snapshot),
"ely://plugins" => self.render_plugin_catalog_page(snapshot, cx),
url if url.starts_with("ely://crash/") => self.render_crash_route(snapshot, url, cx),
url if url.starts_with("ely://plugin/") => {
self.render_plugin_detail_page(snapshot, url, cx)
}
@@ -0,0 +1,201 @@
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, StyledExt,
button::{Button, ButtonVariants},
};
use super::{ElyShell, render_canvas_surface};
impl ElyShell {
pub(super) fn render_crash_page(
&mut self,
tab: &BrowserTab,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
render_canvas_surface(render_crash_content(tab, snapshot, cx))
}
pub(super) fn render_crash_route(
&mut self,
snapshot: &BrowserSnapshot,
url: &str,
cx: &mut Context<Self>,
) -> AnyElement {
let Some(tab_id) = crash_route_tab_id(url) else {
return render_missing_crash_route("Crash route is invalid.");
};
let Some(tab) = snapshot.tabs.iter().find(|tab| tab.id().as_str() == tab_id) else {
return render_missing_crash_route("Tab could not be found.");
};
render_canvas_surface(render_crash_content(tab, snapshot, cx))
}
}
fn render_crash_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_crash_header(tab, cx))
.child(
div()
.border_t_1()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.py_4()
.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(
"Form restore prompt",
"Session data remains attached to this tab.".to_string(),
)),
)
.child(
div()
.flex()
.items_center()
.gap_3()
.text_sm()
.text_color(rgb(colors::MUTED))
.child(IconName::TriangleAlert)
.child("The renderer stopped for this tab. Restore reloads the saved tab state."),
)
.child(
div().flex().child(
Button::new("restore-crashed-tab")
.primary()
.small()
.icon(IconName::Undo2)
.label("Restore")
.tooltip("Restore tab")
.on_click(cx.listener(move |shell, _, window, cx| {
shell.recover_crashed_tab(&tab_id, window, cx);
})),
),
)
.into_any_element()
}
fn render_crash_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::TriangleAlert)
.child("Tab Recovery"),
)
.child(
div()
.text_sm()
.truncate()
.text_color(rgb(colors::MUTED))
.child(format!("Recovering {}", tab.display_url())),
),
)
.child(
Button::new("restore-crashed-tab-header")
.ghost()
.small()
.icon(IconName::Undo2)
.label("Restore")
.tooltip("Restore tab")
.on_click(cx.listener(move |shell, _, window, cx| {
shell.recover_crashed_tab(&tab_id, window, cx);
})),
)
.into_any_element()
}
fn render_missing_crash_route(message: &'static str) -> AnyElement {
render_canvas_surface(
div()
.size_full()
.p_8()
.flex()
.flex_col()
.gap_4()
.child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Tab Recovery"))
.child(div().text_sm().text_color(rgb(colors::MUTED)).child(message)),
)
}
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)
}
+1
View File
@@ -1,5 +1,6 @@
mod archive_labels;
mod bookmarks;
mod crashes;
mod downloads;
mod history;
mod internal_pages;