mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-25 00:02:29 +00:00
feat(right-panel): redesign the detail panel and chrome icons
Rework the detail panel's four tabs and the window chrome to read as a deliberate, commercial-grade surface while keeping the terminal's calm. - Chrome icons: filled-block panel toggles; Outline gets a list glyph, Changes a git-branch, Info a redrawn mark; glyphs go to 18px on the full foreground instead of the faint 15px secondary tone. - Panel body: labelled Session / Processes / Ports bands, mono values, pid/port pills, status dots on Outline, neutral M/U badges on Changes, live counts in the headers, and roomier rows. Hue only ever lands on the git add / remove / fail semantics, everything else stays neutral grey. - Fixes: the corner "…" no longer jumps a pixel when the panel opens (the top strip mirrors the TitleBar's hidden bottom border); that strip now drags the window and double-click zooms, like the rail's; and the process list stops flickering — the poll guard now spans the whole 2s cycle instead of being restarted by every repaint between ticks.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><path d="M12 11.25v5"/><circle cx="12" cy="7.6" r="0.95" fill="currentColor" stroke="none"/></svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="4"/><rect x="5.6" y="5.6" width="5.2" height="12.8" rx="1.8" fill="currentColor" stroke="none"/></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="4"/><rect x="13.2" y="5.6" width="5.2" height="12.8" rx="1.8" fill="currentColor" stroke="none"/></svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -44,6 +44,19 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> {
|
||||
// Lucide's `git-branch`, for the sidebar row's branch line (gpui-component
|
||||
// doesn't bundle a git glyph).
|
||||
"icons/git-branch.svg" => include_bytes!("../../assets/icons/git-branch.svg"),
|
||||
// Filled-panel dock glyphs for the window chrome: an outlined frame with
|
||||
// one inset solid block marking which dock is open (the "commercial-app"
|
||||
// look). gpui-component only ships the hollow line `panel-left/right`, so
|
||||
// tty7 carries its own filled variants for the title bar controls.
|
||||
"icons/panel-left.svg" => include_bytes!("../../assets/icons/panel-left.svg"),
|
||||
"icons/panel-right.svg" => include_bytes!("../../assets/icons/panel-right.svg"),
|
||||
// Lucide's `list`, for the detail panel's Outline tab: the recorded
|
||||
// commands are a plain list, not a terminal (`square-terminal` read as
|
||||
// "this is a shell", which every pane already is).
|
||||
"icons/list.svg" => include_bytes!("../../assets/icons/list.svg"),
|
||||
// A refined `info` glyph for the Info tab: a solid dot over a round-capped
|
||||
// stem in a slightly inset circle, cleaner than the stock hairline `i`.
|
||||
"icons/info.svg" => include_bytes!("../../assets/icons/info.svg"),
|
||||
"icons/agents/claude.svg" => include_bytes!("../../assets/icons/agents/claude.svg"),
|
||||
"icons/agents/codex.svg" => include_bytes!("../../assets/icons/agents/codex.svg"),
|
||||
"icons/agents/gemini.svg" => include_bytes!("../../assets/icons/agents/gemini.svg"),
|
||||
|
||||
+4
-2
@@ -945,7 +945,7 @@ impl Tty7App {
|
||||
.gap_1()
|
||||
.pl(px(6.0 + row.depth as f32 * INDENT))
|
||||
.pr_1()
|
||||
.py_0p5()
|
||||
.py_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
// Soft inset-pill highlight on the content surface.
|
||||
@@ -953,8 +953,10 @@ impl Tty7App {
|
||||
.when(!selected, |d| {
|
||||
d.hover(|s| s.bg(cx.theme().accent.opacity(0.5)))
|
||||
})
|
||||
// Folders take the full foreground, files the muted tone — a neutral
|
||||
// weight difference, no hue, so the tree keeps the terminal's calm.
|
||||
.child(Icon::new(icon).xsmall().text_color(if is_dir {
|
||||
cx.theme().primary
|
||||
cx.theme().foreground
|
||||
} else {
|
||||
muted
|
||||
}))
|
||||
|
||||
+283
-104
@@ -13,11 +13,15 @@
|
||||
//! sidebar row does, Changes probes the same `git_diff` the diff overlay does, and
|
||||
//! Files renders the same rows as the code panel's tree.
|
||||
|
||||
use gpui::{AnyElement, Context, Window, div, prelude::*, px};
|
||||
use gpui::{AnyElement, Context, MouseButton, Window, WindowControlArea, div, prelude::*, px};
|
||||
use gpui_component::button::Button;
|
||||
use gpui_component::input::Input;
|
||||
use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex};
|
||||
use gpui_component::{
|
||||
ActiveTheme as _, Icon, IconName, InteractiveElementExt as _, Sizable as _, h_flex, v_flex,
|
||||
};
|
||||
use std::cell::Cell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::core::config::{Config, RightPanelTab};
|
||||
use crate::daemon::protocol::PaneProcs;
|
||||
@@ -51,10 +55,17 @@ pub(crate) struct RightPanelState {
|
||||
pub(crate) procs_pane: Option<u64>,
|
||||
/// Last completed process/port query for `procs_pane`.
|
||||
pub(crate) procs: Option<PaneProcs>,
|
||||
/// A query is in flight. Also the poll loop's own guard: the loop reschedules
|
||||
/// itself from the completion handler, so this being set means "a tick is
|
||||
/// already on the way" and a re-render must not start a second chain.
|
||||
/// A poll cycle is live — a query is in flight *or* the inter-tick timer is
|
||||
/// waiting between ticks. The render path checks this before starting the
|
||||
/// loop, so a re-render never starts a second chain. It must stay set across
|
||||
/// the timer too: clearing it the instant a query returned let every repaint
|
||||
/// in the 2s gap kick off another query, collapsing the interval into a tight
|
||||
/// query→notify→repaint→query loop that made the list flicker.
|
||||
pub(crate) procs_loading: bool,
|
||||
/// Bumped on every pane switch to retire the in-flight poll loop: a tick whose
|
||||
/// generation no longer matches drops its result and stops rescheduling, so the
|
||||
/// freshly started loop for the new pane is the only one left running.
|
||||
pub(crate) procs_gen: u64,
|
||||
}
|
||||
|
||||
/// How often the Info tab re-queries processes and ports while it's open. Fast
|
||||
@@ -134,10 +145,43 @@ impl Tty7App {
|
||||
// bolted under the title bar: its surface runs the full height of
|
||||
// the window, and the tab row sits *on* it rather than on the
|
||||
// terminal's bar above a seam.
|
||||
.child(
|
||||
.child({
|
||||
// The top zone sits level with the real `TitleBar`, but the
|
||||
// bar only spans the terminal column — so, exactly like the
|
||||
// rail's top strip (`tab_sidebar`), make this one act like the
|
||||
// title bar it aligns with: drag to move, double-click to zoom.
|
||||
// A press arms a flag and the first *move* starts the window
|
||||
// move, so a plain click on a tab — and a double-click — still
|
||||
// lands intact; the tabs and corner chrome take their own.
|
||||
let should_move = Rc::new(Cell::new(false));
|
||||
h_flex()
|
||||
.id("right-panel-titlebar-drag")
|
||||
.flex_none()
|
||||
.h(px(crate::ui::app::TITLE_BAR_HEIGHT))
|
||||
// gpui-component's `TitleBar` centres its content inside a
|
||||
// `border_b_1` box — border-box shrinks the content height
|
||||
// by that 1px, nudging its centred glyphs up half a pixel.
|
||||
// The corner chrome (⋯, panel toggle) lives in *both* the
|
||||
// title bar and here, so mirror that hidden border to keep
|
||||
// its centre line identical; without it the glyphs jump
|
||||
// down a physical pixel the moment the panel opens.
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().transparent)
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let should_move = should_move.clone();
|
||||
move |_, _, _| should_move.set(true)
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, {
|
||||
let should_move = should_move.clone();
|
||||
move |_, _, _| should_move.set(false)
|
||||
})
|
||||
.on_mouse_move(move |_, window, _| {
|
||||
if should_move.replace(false) {
|
||||
window.start_window_move();
|
||||
}
|
||||
})
|
||||
.on_double_click(|_, window, _| window.titlebar_double_click())
|
||||
.items_center()
|
||||
.gap(px(2.))
|
||||
.pl(px(CONTENT_INSET - crate::ui::app::TILE_PAD))
|
||||
@@ -145,8 +189,8 @@ impl Tty7App {
|
||||
.child(div().flex_1())
|
||||
// The panel is what reaches the window's right edge while
|
||||
// it's open, so it carries the corner chrome.
|
||||
.child(self.window_chrome(window, cx)),
|
||||
)
|
||||
.child(self.window_chrome(window, cx))
|
||||
})
|
||||
.child(body)
|
||||
.child(handle)
|
||||
.into_any_element(),
|
||||
@@ -252,15 +296,20 @@ impl Tty7App {
|
||||
/// what the icon-only tab row can't. `trailing` carries a tab's own controls
|
||||
/// where it has any, so they sit on the label's line rather than earning a
|
||||
/// second header row.
|
||||
/// A tab's header: the name in a weightier small-caps than the old faint
|
||||
/// label, plus an optional live count trailing it (files, commands, changed
|
||||
/// files) so the header states scale at a glance, and an optional control on
|
||||
/// the right. The count is the quiet mono tally the sidebar group headers use.
|
||||
fn panel_title(
|
||||
&self,
|
||||
text: &str,
|
||||
count: Option<String>,
|
||||
trailing: Option<AnyElement>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.h(px(28.))
|
||||
.h(px(32.))
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.pl(px(CONTENT_INSET))
|
||||
@@ -272,10 +321,25 @@ impl Tty7App {
|
||||
CONTENT_INSET
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(10.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(text.to_uppercase()),
|
||||
h_flex()
|
||||
.items_baseline()
|
||||
.gap(px(7.))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.5))
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
.child(text.to_uppercase()),
|
||||
)
|
||||
.when_some(count, |this, c| {
|
||||
this.child(
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.font_family(cx.theme().mono_font_family.clone())
|
||||
.text_color(cx.theme().muted_foreground.opacity(0.75))
|
||||
.child(c),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(trailing, |this, t| this.child(t))
|
||||
.into_any_element()
|
||||
@@ -366,7 +430,7 @@ impl Tty7App {
|
||||
/// row comes from an accessor the sidebar already uses, so the panel can
|
||||
/// never disagree with the row that spawned it.
|
||||
fn render_panel_info(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let title = self.panel_title("Info", None, cx);
|
||||
let title = self.panel_title("Info", None, None, cx);
|
||||
let mut rows: Vec<(&'static str, String)> = Vec::new();
|
||||
// Held aside from `rows` because they're not key/value lines: the actions
|
||||
// hang off the cwd, and the two lists get their own sub-headers below.
|
||||
@@ -416,25 +480,31 @@ impl Tty7App {
|
||||
// ticking while this tab is the one being looked at.
|
||||
self.sync_procs(pane_id, cx);
|
||||
|
||||
let mut list = v_flex().px(px(CONTENT_INSET)).py(px(2.)).gap(px(5.));
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
let mut list = v_flex().px(px(CONTENT_INSET)).py(px(2.)).gap(px(3.));
|
||||
for (k, v) in rows {
|
||||
list = list.child(
|
||||
h_flex()
|
||||
.items_baseline()
|
||||
.gap(px(8.))
|
||||
.text_size(px(11.5))
|
||||
.gap(px(9.))
|
||||
.py(px(1.))
|
||||
.text_size(px(12.))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(px(52.))
|
||||
.w(px(46.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(k),
|
||||
)
|
||||
.child(
|
||||
// The value is the datum — a path, a branch, a host, a
|
||||
// count — so it takes the mono face, set apart from the
|
||||
// sans key beside it.
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(v),
|
||||
),
|
||||
@@ -442,6 +512,10 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
let inner = v_flex()
|
||||
// Three labelled bands — Session / Processes / Ports — instead of one
|
||||
// flat column, so the pane's facts, what it's running, and what it's
|
||||
// listening on read as distinct groups.
|
||||
.child(self.panel_subtitle("Session", false, cx))
|
||||
.child(list)
|
||||
.when_some(cwd_for_actions, |this, cwd| {
|
||||
this.child(self.cwd_actions(cwd, cx))
|
||||
@@ -504,14 +578,20 @@ impl Tty7App {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// A small caps divider inside a tab's body, for the sub-lists that hang off
|
||||
/// the Info tab. Lighter than [`panel_title`], which is the tab's own header.
|
||||
fn panel_subtitle(&self, text: &str, cx: &mut Context<Self>) -> AnyElement {
|
||||
/// A small-caps band label inside a tab's body, for the sub-lists that hang
|
||||
/// off the Info tab. Lighter than [`panel_title`], which is the tab's own
|
||||
/// header. `divider` draws a hairline above it, so the second and third bands
|
||||
/// separate from the one before; the first band passes `false`.
|
||||
fn panel_subtitle(&self, text: &str, divider: bool, cx: &mut Context<Self>) -> AnyElement {
|
||||
div()
|
||||
.when(divider, |d| {
|
||||
d.mt(px(6.)).border_t_1().border_color(cx.theme().border)
|
||||
})
|
||||
.px(px(CONTENT_INSET))
|
||||
.pt(px(12.))
|
||||
.pb(px(3.))
|
||||
.text_size(px(10.))
|
||||
.pt(px(if divider { 12. } else { 10. }))
|
||||
.pb(px(4.))
|
||||
.text_size(px(10.5))
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(text.to_uppercase())
|
||||
.into_any_element()
|
||||
@@ -525,13 +605,13 @@ impl Tty7App {
|
||||
if procs.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut list = v_flex().px(px(CONTENT_INSET)).gap(px(1.));
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
let mut list = v_flex().px(px(CONTENT_INSET)).py(px(1.)).gap(px(2.));
|
||||
for p in procs {
|
||||
list = list.child(
|
||||
h_flex()
|
||||
.items_baseline()
|
||||
.gap(px(6.))
|
||||
.text_size(px(11.5))
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
@@ -540,6 +620,8 @@ impl Tty7App {
|
||||
// Indent by depth so the tree reads without drawing
|
||||
// connector glyphs into a 260px column.
|
||||
.pl(px(f32::from(p.depth) * 10.))
|
||||
.text_size(px(12.))
|
||||
.font_family(mono.clone())
|
||||
.text_color(if p.foreground {
|
||||
cx.theme().foreground
|
||||
} else {
|
||||
@@ -547,18 +629,17 @@ impl Tty7App {
|
||||
})
|
||||
.child(p.name.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(px(10.5))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(p.pid.to_string()),
|
||||
),
|
||||
.child(info_chip(
|
||||
&p.pid.to_string(),
|
||||
cx.theme().accent,
|
||||
cx.theme().muted_foreground,
|
||||
&mono,
|
||||
)),
|
||||
);
|
||||
}
|
||||
Some(
|
||||
v_flex()
|
||||
.child(self.panel_subtitle("Processes", cx))
|
||||
.child(self.panel_subtitle("Processes", true, cx))
|
||||
.child(list)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -571,25 +652,26 @@ impl Tty7App {
|
||||
if ports.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut list = v_flex().px(px(CONTENT_INSET)).gap(px(1.));
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
let mut list = v_flex().px(px(CONTENT_INSET)).py(px(1.)).gap(px(2.));
|
||||
for p in ports {
|
||||
list = list.child(
|
||||
h_flex()
|
||||
.items_baseline()
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.text_size(px(11.5))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(px(52.))
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(p.port.to_string()),
|
||||
)
|
||||
.child(info_chip(
|
||||
&p.port.to_string(),
|
||||
cx.theme().accent,
|
||||
cx.theme().foreground,
|
||||
&mono,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_size(px(12.))
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(p.name.clone()),
|
||||
),
|
||||
@@ -597,7 +679,7 @@ impl Tty7App {
|
||||
}
|
||||
Some(
|
||||
v_flex()
|
||||
.child(self.panel_subtitle("Ports", cx))
|
||||
.child(self.panel_subtitle("Ports", true, cx))
|
||||
.child(list)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -618,17 +700,24 @@ impl Tty7App {
|
||||
// Drop the previous pane's answer rather than showing it under the new
|
||||
// pane's heading until the first tick lands.
|
||||
self.right_panel.procs = None;
|
||||
// Retire the old pane's loop and free the guard so the new pane's loop
|
||||
// can start below; the retired tick bows out on the generation check.
|
||||
self.right_panel.procs_gen += 1;
|
||||
self.right_panel.procs_loading = false;
|
||||
}
|
||||
if !self.right_panel.procs_loading {
|
||||
self.spawn_procs_query(pane_id, cx);
|
||||
self.right_panel.procs_loading = true;
|
||||
let generation = self.right_panel.procs_gen;
|
||||
self.spawn_procs_query(pane_id, generation, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// One query, then reschedule — the poll loop. It reschedules only while the
|
||||
/// panel is open on Info, so the loop is self-terminating: close the panel or
|
||||
/// switch tabs and the next completion simply doesn't queue another.
|
||||
fn spawn_procs_query(&mut self, pane_id: u64, cx: &mut Context<Self>) {
|
||||
self.right_panel.procs_loading = true;
|
||||
fn spawn_procs_query(&mut self, pane_id: u64, generation: u64, cx: &mut Context<Self>) {
|
||||
// `procs_loading` is set by the caller (`sync_procs`) and deliberately
|
||||
// stays set across the whole cycle, including the timer wait below.
|
||||
cx.spawn(async move |this, cx| {
|
||||
let procs = cx
|
||||
.background_executor()
|
||||
@@ -636,16 +725,21 @@ impl Tty7App {
|
||||
.await;
|
||||
let keep_polling = this
|
||||
.update(cx, |app, cx| {
|
||||
app.right_panel.procs_loading = false;
|
||||
// A pane switch while we flew makes this answer stale; drop it
|
||||
// and let the new pane's own query land.
|
||||
if app.right_panel.procs_pane != Some(pane_id) {
|
||||
// A pane switch while we flew bumped the generation: drop this
|
||||
// answer and leave the guard to whoever owns the new one.
|
||||
if app.right_panel.procs_gen != generation {
|
||||
return false;
|
||||
}
|
||||
app.right_panel.procs = Some(procs);
|
||||
cx.notify();
|
||||
let cfg = cx.global::<Config>();
|
||||
cfg.right_panel_visible && cfg.right_panel_tab == RightPanelTab::Info
|
||||
let wanted =
|
||||
cfg.right_panel_visible && cfg.right_panel_tab == RightPanelTab::Info;
|
||||
if !wanted {
|
||||
// Loop ends here; release the guard so reopening restarts it.
|
||||
app.right_panel.procs_loading = false;
|
||||
}
|
||||
wanted
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !keep_polling {
|
||||
@@ -654,11 +748,16 @@ impl Tty7App {
|
||||
cx.background_executor().timer(PROCS_POLL).await;
|
||||
let _ = this.update(cx, |app, cx| {
|
||||
// Re-check rather than trusting the pre-sleep decision: two seconds
|
||||
// is plenty of time to close the panel.
|
||||
// is plenty of time to switch panes or close the panel.
|
||||
if app.right_panel.procs_gen != generation {
|
||||
return;
|
||||
}
|
||||
let cfg = cx.global::<Config>();
|
||||
let wanted = cfg.right_panel_visible && cfg.right_panel_tab == RightPanelTab::Info;
|
||||
if wanted && app.right_panel.procs_pane == Some(pane_id) {
|
||||
app.spawn_procs_query(pane_id, cx);
|
||||
if wanted {
|
||||
app.spawn_procs_query(pane_id, generation, cx);
|
||||
} else {
|
||||
app.right_panel.procs_loading = false;
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -674,41 +773,59 @@ impl Tty7App {
|
||||
/// Newest first because that's the end you came from: you scrolled past the
|
||||
/// thing you want, and the list should start where your attention is.
|
||||
fn render_panel_outline(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let title = self.panel_title("Outline", None, cx);
|
||||
let Some(leaf) = self
|
||||
.tabs
|
||||
.get(self.active)
|
||||
.and_then(|t| t.detail_pane(window, cx))
|
||||
else {
|
||||
let title = self.panel_title("Outline", None, None, cx);
|
||||
return self.panel_scroll(self.panel_empty("No active session.", cx), title);
|
||||
};
|
||||
let marks = leaf.read(cx).command_marks();
|
||||
if marks.is_empty() {
|
||||
// Count first (a cheap getter) so the borrow ends before `panel_title`
|
||||
// needs `&mut cx`; the list re-borrows the marks below.
|
||||
let count = leaf.read(cx).command_marks().len();
|
||||
if count == 0 {
|
||||
// Two very different causes, one honest sentence: nothing has run
|
||||
// yet, or this shell never reported OSC 133 (no integration, a bare
|
||||
// `sh`, a nested PTY that eats the marks).
|
||||
let title = self.panel_title("Outline", None, None, cx);
|
||||
return self.panel_scroll(
|
||||
self.panel_empty("No commands recorded for this pane.", cx),
|
||||
title,
|
||||
);
|
||||
}
|
||||
let title = self.panel_title("Outline", Some(count.to_string()), None, cx);
|
||||
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
let mut list = v_flex().px(px(CONTENT_INSET - 4.)).py(px(2.)).gap(px(1.));
|
||||
let marks = leaf.read(cx).command_marks();
|
||||
for mark in marks.iter().rev() {
|
||||
let row = mark.row;
|
||||
let leaf = leaf.clone();
|
||||
// A command that failed is the one you're most often looking for, so
|
||||
// it gets the only color in the list.
|
||||
let failed = mark.exit.is_some_and(|c| c != 0);
|
||||
let running = !mark.done;
|
||||
// A leading status marker reads as a shape first: a hollow ring for a
|
||||
// clean finish, a filled dot while it runs, and — the only tinted one
|
||||
// — a danger dot for a nonzero exit. The failure is what you scan for.
|
||||
let dot = {
|
||||
let d = div().flex_none().size(px(7.)).rounded_full();
|
||||
if failed {
|
||||
d.bg(cx.theme().danger)
|
||||
} else if running {
|
||||
d.bg(cx.theme().muted_foreground)
|
||||
} else {
|
||||
d.border_1()
|
||||
.border_color(cx.theme().muted_foreground.opacity(0.55))
|
||||
}
|
||||
};
|
||||
list = list.child(
|
||||
h_flex()
|
||||
.id(gpui::SharedString::from(format!("panel-mark-{row}")))
|
||||
.items_baseline()
|
||||
.gap(px(6.))
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.px(px(4.))
|
||||
.py(px(2.))
|
||||
.rounded(px(4.))
|
||||
.text_size(px(11.5))
|
||||
.py(px(3.))
|
||||
.rounded(px(5.))
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.bg(cx.theme().sidebar_accent.opacity(0.55)))
|
||||
.on_click(cx.listener(move |_this, _, _window, cx| {
|
||||
@@ -716,18 +833,21 @@ impl Tty7App {
|
||||
view.scroll_to_mark(row, cx);
|
||||
});
|
||||
}))
|
||||
.child(dot)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
// Commands are code: the mono face sets them apart from
|
||||
// the sans labels and lines the list up like a log.
|
||||
.text_size(px(12.))
|
||||
.font_family(mono.clone())
|
||||
.text_color(if failed {
|
||||
cx.theme().danger
|
||||
} else {
|
||||
cx.theme().foreground
|
||||
})
|
||||
// Commands wrap in the shell but must not here: one
|
||||
// row per command is what makes the list scannable.
|
||||
.child(one_line(&mark.text)),
|
||||
)
|
||||
// Only nonzero exits earn a badge. Annotating every success
|
||||
@@ -738,20 +858,10 @@ impl Tty7App {
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(px(10.5))
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().danger)
|
||||
.child(code.to_string()),
|
||||
)
|
||||
})
|
||||
// A command still running is worth marking: it's why the
|
||||
// pane is busy.
|
||||
.when(!mark.done, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(px(10.5))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("…"),
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -764,7 +874,6 @@ impl Tty7App {
|
||||
/// diff overlay's hunk cards, which need far more than 260px to be readable.
|
||||
/// Clicking a row opens the full overlay on that repo.
|
||||
fn render_panel_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let title = self.panel_title("Changes", None, cx);
|
||||
let cwd = self
|
||||
.tabs
|
||||
.get(self.active)
|
||||
@@ -777,6 +886,7 @@ impl Tty7App {
|
||||
});
|
||||
|
||||
let Some(cwd) = cwd else {
|
||||
let title = self.panel_title("Changes", None, None, cx);
|
||||
return self.panel_scroll(self.panel_empty("No working directory.", cx), title);
|
||||
};
|
||||
// Probe on first paint for this cwd, and whenever the pane moves to a
|
||||
@@ -788,6 +898,18 @@ impl Tty7App {
|
||||
self.spawn_right_panel_diff(cwd.clone(), cx);
|
||||
}
|
||||
|
||||
// Count of changed files for the header tally — computed before the title
|
||||
// so the diff borrow ends before `panel_title` takes `&mut cx`.
|
||||
let count = match &self.right_panel.diff {
|
||||
Some(Some(snap)) => {
|
||||
let n = snap.files.len() + snap.untracked.len();
|
||||
(n > 0).then(|| n.to_string())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let title = self.panel_title("Changes", count, None, cx);
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
|
||||
let inner = match &self.right_panel.diff {
|
||||
None => self.panel_empty("Loading…", cx),
|
||||
Some(None) => self.panel_empty("Not a git work tree.", cx),
|
||||
@@ -811,12 +933,11 @@ impl Tty7App {
|
||||
list = list.child(
|
||||
h_flex()
|
||||
.id(gpui::SharedString::from(format!("panel-change-{path}")))
|
||||
.items_baseline()
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.px(px(4.))
|
||||
.py(px(2.))
|
||||
.rounded(px(4.))
|
||||
.text_size(px(11.5))
|
||||
.py(px(3.))
|
||||
.rounded(px(5.))
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.bg(cx.theme().sidebar_accent.opacity(0.55)))
|
||||
.when(selected, |s| s.bg(cx.theme().sidebar_accent))
|
||||
@@ -835,36 +956,62 @@ impl Tty7App {
|
||||
);
|
||||
})
|
||||
})
|
||||
// A neutral status letter, kind by glyph not by hue —
|
||||
// tracked edits are `M`; untracked get `U` below.
|
||||
.child(git_badge("M", cx.theme().muted_foreground, &mono))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_size(px(12.))
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(path),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(cx.theme().success)
|
||||
.child(format!("+{added}")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(format!("−{removed}")),
|
||||
),
|
||||
// +N / −M keep the terminal-git greens and reds, the
|
||||
// one place hue earns its keep; a zero side is dropped
|
||||
// rather than shown as `+0`.
|
||||
.when(added > 0, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(px(11.))
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().success)
|
||||
.child(format!("+{added}")),
|
||||
)
|
||||
})
|
||||
.when(removed > 0, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(px(11.))
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().danger)
|
||||
.child(format!("−{removed}")),
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
if !untracked.is_empty() {
|
||||
list = list.child(
|
||||
div()
|
||||
.pt(px(4.))
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.px(px(4.))
|
||||
.text_size(px(11.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!("{} untracked", untracked.len())),
|
||||
.py(px(3.))
|
||||
.child(git_badge(
|
||||
"U",
|
||||
cx.theme().muted_foreground.opacity(0.75),
|
||||
&mono,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.5))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!("{} untracked", untracked.len())),
|
||||
),
|
||||
);
|
||||
}
|
||||
list.into_any_element()
|
||||
@@ -913,7 +1060,7 @@ impl Tty7App {
|
||||
/// views of one tree rather than two trees.
|
||||
fn render_panel_files(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let controls = self.files_controls(cx);
|
||||
let title = self.panel_title("Files", Some(controls), cx);
|
||||
let title = self.panel_title("Files", None, Some(controls), cx);
|
||||
let search = self.files_search(cx);
|
||||
let rows = self.render_file_tree_rows(window, cx);
|
||||
v_flex()
|
||||
@@ -926,6 +1073,38 @@ impl Tty7App {
|
||||
}
|
||||
}
|
||||
|
||||
/// A small status letter (`M`/`U`/…) for a change row. The *kind* is told by the
|
||||
/// glyph in the mono face, not by colour, so the list stays monochrome; callers
|
||||
/// pass a muted tone and reserve real hue for the `+N −M` counts beside it.
|
||||
fn git_badge(letter: &str, color: gpui::Hsla, mono: &gpui::SharedString) -> AnyElement {
|
||||
div()
|
||||
.flex_none()
|
||||
.w(px(14.))
|
||||
.text_center()
|
||||
.text_size(px(10.5))
|
||||
.font_family(mono.clone())
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_color(color)
|
||||
.child(letter.to_string())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// A pid / port pill: a mono number on the soft-grey capsule the rest of the
|
||||
/// chrome uses, so a numeric datum reads as a tag rather than loose text.
|
||||
fn info_chip(text: &str, bg: gpui::Hsla, fg: gpui::Hsla, mono: &gpui::SharedString) -> AnyElement {
|
||||
div()
|
||||
.flex_none()
|
||||
.px(px(5.))
|
||||
.py(px(1.5))
|
||||
.rounded(px(4.))
|
||||
.bg(bg)
|
||||
.text_size(px(10.5))
|
||||
.font_family(mono.clone())
|
||||
.text_color(fg)
|
||||
.child(text.to_string())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The one-word status the Info row shows next to the agent's name.
|
||||
fn agent_status_label(status: crate::core::cli_agent::AgentStatus) -> &'static str {
|
||||
use crate::core::cli_agent::AgentStatus::*;
|
||||
|
||||
@@ -748,11 +748,11 @@ impl Tty7App {
|
||||
.child(
|
||||
self.attach_new_tab_menu(
|
||||
Button::new("sidebar-add")
|
||||
.icon(Icon::new(IconName::Plus).size(px(15.)))
|
||||
.icon(Icon::new(IconName::Plus).size(px(18.)))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg(),
|
||||
cx,
|
||||
),
|
||||
@@ -760,13 +760,13 @@ impl Tty7App {
|
||||
.child(
|
||||
crate::ui::tab_strip::chrome_tile(
|
||||
Button::new("sidebar-collapse")
|
||||
.icon(Icon::new(IconName::PanelLeft).size(px(15.))),
|
||||
.icon(Icon::empty().path("icons/panel-left.svg").size(px(18.))),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg()
|
||||
.tooltip("Hide Sidebar")
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))),
|
||||
|
||||
+45
-24
@@ -164,7 +164,10 @@ pub(crate) fn chrome_tile_variant(cx: &gpui::App) -> ButtonCustomVariant {
|
||||
let accent = cx.theme().sidebar_accent;
|
||||
ButtonCustomVariant::new(cx)
|
||||
.color(cx.theme().transparent)
|
||||
.foreground(cx.theme().secondary_foreground)
|
||||
// Full `foreground`, not the softer `secondary_foreground`: the chrome
|
||||
// glyphs read as deliberate controls rather than faint hints — the
|
||||
// "commercial-app" weight, paired with the filled dock icons below.
|
||||
.foreground(cx.theme().foreground)
|
||||
.hover(accent.opacity(0.55))
|
||||
.active(accent)
|
||||
}
|
||||
@@ -213,13 +216,13 @@ impl Tty7App {
|
||||
div().occlude().flex_shrink_0().child(
|
||||
chrome_tile(
|
||||
Button::new("titlebar-right-panel")
|
||||
.icon(Icon::new(IconName::PanelRight).size(px(15.))),
|
||||
.icon(Icon::empty().path("icons/panel-right.svg").size(px(18.))),
|
||||
panel_open,
|
||||
cx,
|
||||
)
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg()
|
||||
.tooltip("Detail Panel")
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
@@ -231,13 +234,13 @@ impl Tty7App {
|
||||
div().occlude().flex_shrink_0().child(
|
||||
chrome_tile(
|
||||
Button::new("titlebar-menu")
|
||||
.icon(Icon::new(IconName::Ellipsis).size(px(15.))),
|
||||
.icon(Icon::new(IconName::Ellipsis).size(px(18.))),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg()
|
||||
.dropdown_menu_with_anchor(
|
||||
gpui::Anchor::TopRight,
|
||||
@@ -257,10 +260,29 @@ impl Tty7App {
|
||||
pub(crate) fn right_panel_tabs(&self, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
||||
let active_tab = cx.global::<Config>().right_panel_tab;
|
||||
[
|
||||
(RightPanelTab::Info, IconName::Info, "Info"),
|
||||
(RightPanelTab::Outline, IconName::SquareTerminal, "Outline"),
|
||||
(RightPanelTab::Changes, IconName::Replace, "Changes"),
|
||||
(RightPanelTab::Files, IconName::FolderClosed, "Files"),
|
||||
(
|
||||
RightPanelTab::Info,
|
||||
Icon::empty().path("icons/info.svg"),
|
||||
"Info",
|
||||
),
|
||||
(
|
||||
RightPanelTab::Outline,
|
||||
Icon::empty().path("icons/list.svg"),
|
||||
"Outline",
|
||||
),
|
||||
// git-branch (from tty7's own assets) instead of the abstract
|
||||
// `Replace` glyph: Changes is a working-tree diff, and the branch
|
||||
// mark reads as version control at a glance — matching the mockup.
|
||||
(
|
||||
RightPanelTab::Changes,
|
||||
Icon::empty().path("icons/git-branch.svg"),
|
||||
"Changes",
|
||||
),
|
||||
(
|
||||
RightPanelTab::Files,
|
||||
Icon::new(IconName::FolderClosed),
|
||||
"Files",
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(tab, icon, label)| {
|
||||
@@ -269,14 +291,13 @@ impl Tty7App {
|
||||
.flex_shrink_0()
|
||||
.child(
|
||||
chrome_tile(
|
||||
Button::new(("right-panel-tab", tab as usize))
|
||||
.icon(Icon::new(icon).size(px(15.))),
|
||||
Button::new(("right-panel-tab", tab as usize)).icon(icon.size(px(18.))),
|
||||
active_tab == tab,
|
||||
cx,
|
||||
)
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg()
|
||||
.tooltip(label)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
@@ -1061,13 +1082,13 @@ impl Tty7App {
|
||||
self.attach_new_tab_menu(
|
||||
chrome_tile(
|
||||
Button::new("tab-add")
|
||||
.icon(Icon::new(IconName::Plus).size(px(15.))),
|
||||
.icon(Icon::new(IconName::Plus).size(px(18.))),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg(),
|
||||
cx,
|
||||
),
|
||||
@@ -1091,13 +1112,13 @@ impl Tty7App {
|
||||
self.attach_new_tab_menu(
|
||||
chrome_tile(
|
||||
Button::new("titlebar-add-collapsed")
|
||||
.icon(Icon::new(IconName::Plus).size(px(15.))),
|
||||
.icon(Icon::new(IconName::Plus).size(px(18.))),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg(),
|
||||
cx,
|
||||
),
|
||||
@@ -1107,13 +1128,13 @@ impl Tty7App {
|
||||
div().occlude().flex_shrink_0().child(
|
||||
chrome_tile(
|
||||
Button::new("titlebar-expand-sidebar")
|
||||
.icon(Icon::new(IconName::PanelLeft).size(px(15.))),
|
||||
.icon(Icon::empty().path("icons/panel-left.svg").size(px(18.))),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.xsmall()
|
||||
.w(px(30.))
|
||||
.h(px(30.))
|
||||
.w(px(32.))
|
||||
.h(px(32.))
|
||||
.rounded_lg()
|
||||
.tooltip("Show Sidebar")
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.toggle_left_panel(cx))),
|
||||
|
||||
Reference in New Issue
Block a user