From bb7078cfd09b92fa922da641ff19dfd310399832 Mon Sep 17 00:00:00 2001 From: hhdebb <72241812+hhdebb@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:18:45 +0800 Subject: [PATCH] fix(sidebar): let an agent's status dot sit outside its disc (#846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `status_dot` places itself with negative offsets so that it overhangs the avatar's edge — that overhang is what makes it read as a badge sitting on the disc rather than a notch bitten out of it. But it is added as a child of the element carrying `rounded_full`, so everything past the circle is clipped along the arc and the badge comes back as a crescent. Give the wrapper the positioning and move the disc down a level. The radius then only ever clips the disc's own fill and mark, and the dot becomes a sibling drawn after it, which settles the paint order too. No test: the change is structural — which element carries the radius — and the tests here cover the pure decisions behind the avatar (`needs_edge`, `agent_status_label`) rather than its element tree. --- src/ui/tab_strip.rs | 86 +++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index 10d55caf..e023e831 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -1319,13 +1319,22 @@ impl Tty7App { size: f32, cx: &App, ) -> gpui::AnyElement { - let base = div() - .id(id) - .flex_shrink_0() - .size(px(size)) - .flex() - .items_center() - .justify_center(); + // The wrapper positions; the disc below carries the radius. + // `status_dot` hangs itself off the edge with negative offsets — that + // overhang is what makes it a badge on the avatar rather than a notch + // in it — and as a child of the rounded element the overhang was + // clipped along the arc, leaving a crescent. + let base = div().id(id).flex_shrink_0().relative().size(px(size)); + // Fill, hairline and mark all live here, so the radius only ever clips + // the disc's own paint. + let disc = || { + div() + .size(px(size)) + .flex() + .items_center() + .justify_center() + .rounded_full() + }; match agent { Some(agent) => { let hollow = status == Some(crate::core::cli_agent::AgentStatus::Waiting); @@ -1344,40 +1353,41 @@ impl Tty7App { // column of twenty. let accent = agent.accent_rgb(); let surface = cx.theme().background; - base.relative() - .rounded_full() - .bg(gpui::rgb(accent)) - // Codex and Grok are both pure black, which is the window - // fill on a dark theme — the disc dissolves and leaves the - // glyph floating. A hairline keeps it a disc in any theme. - .when(crate::ui::presets::needs_edge(accent, surface), |d| { - d.border_1().border_color(cx.theme().border) - }) - .child( - gpui::svg() - .path(agent.icon_path()) - .size(px(size * 0.54)) - // SVG assets render as a single-colour mask, so - // the mark's colour comes from the agent rather - // than from the file. The tray icon reads the same - // answer. - .text_color(gpui::rgb(agent.icon_rgb())), - ) - .when_some(dot, |b, dot| b.child(dot)) - .tooltip(move |window, cx| { - gpui_component::tooltip::Tooltip::new(tip.clone()).build(window, cx) - }) - .into_any_element() + base.child( + disc() + .bg(gpui::rgb(accent)) + // Codex and Grok are both pure black, which is the + // window fill on a dark theme — the disc dissolves and + // leaves the glyph floating. A hairline keeps it a disc + // in any theme. + .when(crate::ui::presets::needs_edge(accent, surface), |d| { + d.border_1().border_color(cx.theme().border) + }) + .child( + gpui::svg() + .path(agent.icon_path()) + .size(px(size * 0.54)) + // SVG assets render as a single-colour mask, so + // the mark's colour comes from the agent rather + // than from the file. The tray icon reads the + // same answer. + .text_color(gpui::rgb(agent.icon_rgb())), + ), + ) + .when_some(dot, |b, dot| b.child(dot)) + .tooltip(move |window, cx| { + gpui_component::tooltip::Tooltip::new(tip.clone()).build(window, cx) + }) + .into_any_element() } None => base - .relative() - .rounded_full() - .bg(cx.theme().muted) .child( - gpui::svg() - .path("icons/terminal.svg") - .size(px(size * 0.56)) - .text_color(cx.theme().foreground.opacity(0.65)), + disc().bg(cx.theme().muted).child( + gpui::svg() + .path("icons/terminal.svg") + .size(px(size * 0.56)) + .text_color(cx.theme().foreground.opacity(0.65)), + ), ) .when_some(ssh, |b, rgb| { b.child(Self::status_dot(rgb, 0, size, cx.theme().background, false))