diff --git a/Cargo.lock b/Cargo.lock index aab1fcaf..7080e343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2922,7 +2922,7 @@ dependencies = [ [[package]] name = "gpui-component" version = "0.5.2" -source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#25079a7b2519b1da1f7aced160725a852643b50a" +source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#9484cf99c89ac7bbce7af063c329925a6576409b" dependencies = [ "aho-corasick", "anyhow", @@ -2972,7 +2972,7 @@ dependencies = [ [[package]] name = "gpui-component-assets" version = "0.5.1" -source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#25079a7b2519b1da1f7aced160725a852643b50a" +source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#9484cf99c89ac7bbce7af063c329925a6576409b" dependencies = [ "anyhow", "gpui", @@ -2986,7 +2986,7 @@ dependencies = [ [[package]] name = "gpui-component-macros" version = "0.5.1" -source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#25079a7b2519b1da1f7aced160725a852643b50a" +source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#9484cf99c89ac7bbce7af063c329925a6576409b" dependencies = [ "proc-macro2", "quote", diff --git a/src/ui/forwards.rs b/src/ui/forwards.rs index 1a078268..82310419 100644 --- a/src/ui/forwards.rs +++ b/src/ui/forwards.rs @@ -10,31 +10,24 @@ use gpui_component::button::{Button, ButtonVariants as _}; use gpui_component::input::Input; use gpui_component::{ActiveTheme as _, IconName, Sizable as _, h_flex, v_flex}; -use crate::daemon::protocol::{ - ForwardStatus, ManagedForward, RemoteContext, SshForwardKind, SshPhase, -}; +use crate::daemon::protocol::{ForwardStatus, ManagedForward, RemoteContext, SshForwardKind}; use crate::terminal::view::TerminalView; use crate::ui::app::Tty7App; impl Tty7App { - /// The in-pane native-SSH status strip (PRD FR-E1): a subtle ` SSH ` chip - /// coloured by the connection phase, with the hostname, pinned top-left of the - /// terminal body. A dead pane also shows the "connection lost — ⌘⇧R to - /// reconnect" notice (FR-E4). Returns `None` for a non-native pane. + /// The in-pane native-SSH notice (PRD FR-E4): a dead pane shows a + /// bottom-centered "Disconnected — ⌘⇧R to reconnect" bar. Live/connecting + /// panes show nothing here — the tab status dot already carries the phase and + /// the daemon prints connect progress/failures into the buffer. Returns + /// `None` for a non-native or still-alive pane. pub(crate) fn render_ssh_status_strip( &self, leaf: &Entity, cx: &mut Context, ) -> Option { let view = leaf.read(cx); - let phase = view.ssh_phase()?; - let disconnected = view.ssh_disconnected(); - // Once connected, the tab status dot already carries the connection state and - // the top-right tunnel/SFTP icons signal "this is an SSH pane" — an in-pane - // chip here would just float over the shell output. Only surface the strip - // while still connecting (blank pane, no overlap) or after a drop (the - // actionable reconnect notice). - if matches!(phase, SshPhase::Connected) && !disconnected { + view.ssh_phase()?; + if !view.ssh_disconnected() { return None; } let host = view @@ -45,69 +38,54 @@ impl Tty7App { .unwrap_or_default(); let theme = cx.theme(); - // Phase → accent. Connecting/authenticating are cautionary (yellow), - // connected reads calm (accent), failed/disconnected are red. - let (color, label) = if disconnected { - (theme.danger, "SSH ✕") - } else { - match &phase { - SshPhase::Connecting => (theme.warning, "SSH …"), - SshPhase::Authenticating => (theme.warning, "SSH ⚿"), - SshPhase::Connected => (theme.accent, "SSH"), - SshPhase::Failed { .. } => (theme.danger, "SSH ✕"), - } - }; - let chip = h_flex() + // The failure reason is already printed into the terminal buffer + // (top-left, in red) by the daemon, so a top-left overlay would sit right + // on top of it. Dock the (actionable) reconnect notice at the + // bottom-center — clear of the output, a familiar "connection lost, + // reconnect" spot. + let bar = h_flex() + .occlude() .items_center() - .gap_1p5() - .px_2() - .py_0p5() - .rounded_md() - .bg(color.opacity(0.15)) + .gap_2() + .px_3() + .py_1p5() + .rounded_lg() + .bg(theme.popover) .border_1() - .border_color(color.opacity(0.5)) + .border_color(theme.danger.opacity(0.4)) + .shadow_md() .text_xs() - .text_color(color) - .child(div().font_weight(FontWeight::SEMIBOLD).child(label)) - .when(!host.is_empty(), |d| { - d.child(div().text_color(theme.muted_foreground).child(host)) - }); - - let mut col = div().flex().flex_col().items_start().gap_1().child(chip); - - if disconnected { - col = col.child( - h_flex() - .items_center() - .gap_2() - .px_2() - .py_1() - .rounded_md() - .bg(theme.danger.opacity(0.12)) - .border_1() - .border_color(theme.danger.opacity(0.4)) - .text_xs() + .text_color(theme.muted_foreground) + .child( + div() + .font_weight(FontWeight::MEDIUM) .text_color(theme.foreground) - .child("Connection lost — press ⌘⇧R to reconnect") - .child( - Button::new("ssh-reconnect") - .label("Reconnect") - .primary() - .small() - .on_click(cx.listener(|this, _, window, cx| { - this.restart_ssh_session(window, cx) - })), + .child(if host.is_empty() { + "Disconnected".to_string() + } else { + format!("Disconnected from {host}") + }), + ) + .child(div().child("· ⌘⇧R")) + .child( + Button::new("ssh-reconnect") + .label("Reconnect") + .primary() + .small() + .on_click( + cx.listener(|this, _, window, cx| this.restart_ssh_session(window, cx)), ), ); - } - Some( div() .absolute() - .top_2() - .left_4() - .child(col) + .left_0() + .right_0() + .bottom_4() + .flex() + .justify_center() + .child(bar) .into_any_element(), ) } diff --git a/src/ui/presets.rs b/src/ui/presets.rs index 61164af6..6283acdd 100644 --- a/src/ui/presets.rs +++ b/src/ui/presets.rs @@ -181,7 +181,7 @@ impl Theme { } /// Blend `a` toward `b` by `t` (0.0 = all `a`, 1.0 = all `b`), per channel. -fn mix(a: u32, b: u32, t: f32) -> u32 { +pub(crate) fn mix(a: u32, b: u32, t: f32) -> u32 { let (ar, ag, ab) = (a >> 16 & 0xff, a >> 8 & 0xff, a & 0xff); let (br, bg, bb) = (b >> 16 & 0xff, b >> 8 & 0xff, b & 0xff); let ch = |x: u32, y: u32| (x as f32 + (y as f32 - x as f32) * t).round() as u32; diff --git a/src/ui/ssh_prompt.rs b/src/ui/ssh_prompt.rs index 3842028e..6ba2a66e 100644 --- a/src/ui/ssh_prompt.rs +++ b/src/ui/ssh_prompt.rs @@ -21,8 +21,9 @@ use gpui::{ Subscription, Window, div, prelude::*, px, }; use gpui_component::button::{Button, ButtonVariants as _}; +use gpui_component::checkbox::Checkbox; use gpui_component::input::{Input, InputEvent, InputState}; -use gpui_component::{ActiveTheme as _, Selectable as _, Sizable as _, h_flex, v_flex}; +use gpui_component::{ActiveTheme as _, Sizable as _, h_flex, v_flex}; use crate::core::keychain::{CredentialStore as _, OsCredentialStore}; use crate::daemon::protocol::{AuthPromptKind, AuthResponse, SshPhase}; @@ -668,9 +669,9 @@ impl Tty7App { .occlude() .track_focus(&self.ssh_prompt.focus_handle) .key_context("SshPrompt") - .w(px(460.)) - .gap_2() - .p_3() + .w(px(420.)) + .gap_3() + .p_4() .bg(cx.theme().popover) .border_1() .rounded_lg() @@ -835,16 +836,16 @@ impl Tty7App { } fn render_ssh_remember(&self, cx: &mut Context) -> AnyElement { - Button::new("ssh-remember") - .label(if self.ssh_prompt.remember { - "☑ Remember (keychain)" - } else { - "☐ Remember (keychain)" - }) - .small() - .ghost() - .selected(self.ssh_prompt.remember) - .on_click(cx.listener(|this, _, _w, cx| this.toggle_ssh_remember(cx))) + // A real checkbox, left-aligned in its own row. The old ghost Button + // stretched to the card's full width, so its selected-state fill read as + // a full-width grey bar rather than a checkbox. + h_flex() + .child( + Checkbox::new("ssh-remember") + .label("Remember (keychain)") + .checked(self.ssh_prompt.remember) + .on_click(cx.listener(|this, _, _w, cx| this.toggle_ssh_remember(cx))), + ) .into_any_element() } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index a31bfa39..c0d31b5f 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -136,6 +136,28 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) { t.tokens.accent = Hsla::from(accent_fill).into(); t.tokens.accent_foreground = accent_text.into(); + // Primary buttons (Connect, Reconnect, Trust, Save…) fill from + // `tokens.button_primary`; the raw default is the foreground — a pure + // near-black in a light theme, which reads harsh. Nudge it toward the + // background so it lands on a softer dark charcoal (and, symmetrically, a + // slightly dimmed near-white in dark themes). We set the whole primary token + // family (both the plain `primary*` fields — used for the border and outline + // text — and the `button_primary*` tokens the fill actually reads) so every + // primary button shifts together, hover/pressed included. The stock + // `button_primary_foreground` stays legible on top either way. + let primary_base: Hsla = rgb(presets::mix(m.foreground, m.background, 0.20)).into(); + let primary_hover: Hsla = rgb(presets::mix(m.foreground, m.background, 0.30)).into(); + let primary_active: Hsla = rgb(presets::mix(m.foreground, m.background, 0.10)).into(); + t.primary = primary_base; + t.primary_hover = primary_hover; + t.primary_active = primary_active; + t.tokens.primary = primary_base.into(); + t.tokens.primary_hover = primary_hover.into(); + t.tokens.primary_active = primary_active.into(); + t.tokens.button_primary = primary_base.into(); + t.tokens.button_primary_hover = primary_hover.into(); + t.tokens.button_primary_active = primary_active.into(); + t.caret = rgb(m.caret).into(); t.selection = rgb(m.selection).into(); // text selection highlight