Merge pull request #161 from l0ng-ai/feat/detail-panel-icons

feat(icons): draw the detail panel and chrome glyphs to one spec
This commit is contained in:
l0ng-ai
2026-07-25 10:32:16 +08:00
committed by GitHub
15 changed files with 108 additions and 20 deletions
+1
View File
@@ -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

+1
View File
@@ -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="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="8.6" y="8.6" width="12.2" height="12.2" rx="3.2"/><path d="M5.8 15.4A2.6 2.6 0 0 1 3.2 12.8V5.8a2.6 2.6 0 0 1 2.6-2.6h7a2.6 2.6 0 0 1 2.6 2.6"/></svg>

After

Width:  |  Height:  |  Size: 343 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>

After

Width:  |  Height:  |  Size: 216 B

+1
View File
@@ -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="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M2.6 12c2.45-4.35 5.58-6.5 9.4-6.5s6.95 2.15 9.4 6.5c-2.45 4.35-5.58 6.5-9.4 6.5S5.05 16.35 2.6 12Z"/><circle cx="12" cy="12" r="2.75" fill="currentColor" stroke="none"/></svg>

After

Width:  |  Height:  |  Size: 369 B

+1
View File
@@ -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="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M13.4 3.2H7.8a2.6 2.6 0 0 0-2.6 2.6v12.4a2.6 2.6 0 0 0 2.6 2.6h8.4a2.6 2.6 0 0 0 2.6-2.6V8Z"/><path d="M13.4 3.2v2.2a2.6 2.6 0 0 0 2.6 2.6h2.8"/></svg>

After

Width:  |  Height:  |  Size: 344 B

+1
View File
@@ -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="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 7A2.6 2.6 0 0 1 5.8 4.4h3.5a2.6 2.6 0 0 1 2.08 1.04l1.04 1.39a1.6 1.6 0 0 0 1.28.64h4.5a2.6 2.6 0 0 1 2.6 2.6v7.93a2.6 2.6 0 0 1-2.6 2.6H5.8a2.6 2.6 0 0 1-2.6-2.6Z"/></svg>

After

Width:  |  Height:  |  Size: 370 B

+1
View File
@@ -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="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 18.4V7A2.6 2.6 0 0 1 5.8 4.4h3.5a2.6 2.6 0 0 1 2.08 1.04l1.04 1.39a1.6 1.6 0 0 0 1.28.64h4.5a2.6 2.6 0 0 1 2.6 2.6v1.13"/><path d="M5.5 20.6h12.34a2.6 2.6 0 0 0 2.47-1.78l1.92-5.76a1.3 1.3 0 0 0-1.23-1.71H7.86a2.6 2.6 0 0 0-2.47 1.78l-1.92 5.76a1.3 1.3 0 0 0 1.23 1.71Z"/></svg>

After

Width:  |  Height:  |  Size: 476 B

+1
View File
@@ -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="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 7A2.6 2.6 0 0 1 5.8 4.4h3.5a2.6 2.6 0 0 1 2.08 1.04l1.04 1.39a1.6 1.6 0 0 0 1.28.64h4.5a2.6 2.6 0 0 1 2.6 2.6v7.93a2.6 2.6 0 0 1-2.6 2.6H5.8a2.6 2.6 0 0 1-2.6-2.6Z"/></svg>

After

Width:  |  Height:  |  Size: 370 B

+1 -1
View File
@@ -1 +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.25" stroke-linecap="round" stroke-linejoin="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M6.2 7.6v8.8"/><path d="M6.2 12h8.6a3 3 0 0 0 3-3"/><circle cx="6.2" cy="5" r="2.6" fill="currentColor" stroke="none"/><circle cx="6.2" cy="19" r="2.6" fill="currentColor" stroke="none"/><circle cx="17.8" cy="6.4" r="2.6" fill="currentColor" stroke="none"/></svg>

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 456 B

+1 -1
View File
@@ -1 +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>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3.2" y="4.6" width="17.6" height="14.8" rx="3"/><path d="M7 10.4h10"/><path d="M7 14.8h5.8"/></svg>

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 292 B

+1 -1
View File
@@ -1 +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>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5.6h11.8"/><path d="M9 12h11.8"/><path d="M9 18.4h7"/><circle cx="4" cy="5.6" r="1.35" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.35" fill="currentColor" stroke="none"/><circle cx="4" cy="18.4" r="1.35" fill="currentColor" stroke="none"/></svg>

Before

Width:  |  Height:  |  Size: 418 B

After

Width:  |  Height:  |  Size: 460 B

+1
View File
@@ -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.4" stroke-linecap="round"><path d="M12 6v12"/><path d="M6 12h12"/></svg>

After

Width:  |  Height:  |  Size: 206 B

+1
View File
@@ -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="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.8" cy="10.8" r="7.4"/><path d="M16.3 16.3 20.8 20.8"/></svg>

After

Width:  |  Height:  |  Size: 259 B

+70 -7
View File
@@ -35,14 +35,28 @@ impl AssetSource for Assets {
/// The bytes of a bundled agent brand mark, or `None` if `path` isn't one of
/// ours. Kept as an explicit match (rather than `rust-embed`) because the set is
/// tiny and fixed, and `include_bytes!` needs no extra build dependency.
///
/// Note that matching on the *path* makes every arm here an app-wide override,
/// not a local one: `gpui_component_macros::icon_named!` derives `IconName` from
/// the downstream asset filenames, so `IconName::Search.path()` is literally
/// `"icons/search.svg"` and every `Icon::new(IconName::Search)` in the tree —
/// tty7's and gpui-component's own — resolves through the arm below. Adding a
/// name that upstream also ships redraws it everywhere; check the call sites
/// before doing so, and prefer a name upstream *doesn't* use (`circle-info`)
/// when only one place should change.
fn agent_icon(path: &str) -> Option<&'static [u8]> {
let bytes: &'static [u8] = match path {
// Flush `>_` prompt glyph for the plain-shell tab avatar (Lucide's
// unboxed `terminal`, which gpui-component doesn't bundle — it only
// ships the boxed `square-terminal`).
"icons/terminal.svg" => include_bytes!("../../assets/icons/terminal.svg"),
// Lucide's `git-branch`, for the sidebar row's branch line (gpui-component
// doesn't bundle a git glyph).
// A git glyph on the detail-panel spec below (gpui-component bundles
// none). Serves both the sidebar row's branch line and the Changes tab.
// Drawn as an actual commit graph — a trunk with a node at each end,
// branching once — rather than lucide's long arc slung between two
// floating rings: that one is the loosest, most lopsided shape in a row
// of four, and at the sidebar's 11px a stroked ring's hole collapses into
// a blur anyway. Nodes are filled here.
"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"
@@ -50,13 +64,62 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> {
// 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).
// The chrome glyphs that sit beside those dock tiles, redrawn to share
// their weight and their solid-mass vocabulary. Lucide's stock `plus` is
// a thin bare cross that reads far lighter than the framed dock icon
// next to it, and its `ellipsis` strokes three `r=1` circles — at 18px
// the cap overlaps its own fill and the dots blur into grey smudges.
//
// `plus` is the one deliberate departure from the spec below: a bare
// cross has no enclosing shape to carry weight, so it runs stroke 2.4 on
// a tighter 6→18 span. Matching the spec exactly would leave it visibly
// frailer than the framed tile beside it.
"icons/plus.svg" => include_bytes!("../../assets/icons/plus.svg"),
"icons/ellipsis.svg" => include_bytes!("../../assets/icons/ellipsis.svg"),
// The detail panel's own set: four tab tiles at 18px and four controls at
// 13px, all in one panel, so they're drawn to one spec instead of taken
// from lucide as-is.
//
// stroke 1.9 (except `plus` — see above)
// radius 2.63.2, never 2.0
// span 3.2→20.8, both axes; circles widen to ~2.5→21.5, since a
// round shape reads smaller at equal geometry
// nodes always filled, r ≥ 1.35 — a stroked dot hazes below 16px
//
// The stock glyphs share none of that: they mix stroke weights, sit a
// 21-wide circle next to an 18-wide folder, and leave so much dead space
// inside the frame that the row reads as four glyphs from four sets.
//
// Shape choices worth keeping: `info` is a panel with two lines written
// in it — a picture of what the tab actually opens (cwd, shell, branch,
// changes) — rather than the circled `i`, which is the most-drawn icon
// there is and says "help" as readily as "details"; Outline's last row is
// cut short, because three full-width rules read as a hamburger menu
// rather than a list;
// `folder-closed` drops its lid rule, which at 18px only crowded the top
// of the box; `copy`'s back sheet wraps three sides and stops on its own
// curves instead of poking two raw stubs out of an L.
"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/folder-closed.svg" => include_bytes!("../../assets/icons/folder-closed.svg"),
"icons/folder-open.svg" => include_bytes!("../../assets/icons/folder-open.svg"),
"icons/info.svg" => include_bytes!("../../assets/icons/info.svg"),
"icons/eye.svg" => include_bytes!("../../assets/icons/eye.svg"),
"icons/search.svg" => include_bytes!("../../assets/icons/search.svg"),
"icons/copy.svg" => include_bytes!("../../assets/icons/copy.svg"),
// `folder` and `file` carry no detail-panel role of their own — they're
// here because overriding `folder-open` above would otherwise split the
// file tree down the middle, drawing expanded rows on this spec and
// collapsed ones (and every file) from stock lucide. Same shape as
// `folder-closed`: upstream's only difference between the two is the lid
// rule this set drops anyway.
"icons/folder.svg" => include_bytes!("../../assets/icons/folder.svg"),
"icons/file.svg" => include_bytes!("../../assets/icons/file.svg"),
// The circled `i` that `info.svg` used to be, kept under its own name for
// the Settings nav's About row — there the glyph labels a section rather
// than a detail tab, and "panel with two lines written in it" says nothing
// about *About*. No upstream `IconName` maps here, so it's referenced by
// path (see `settings.rs`).
"icons/circle-info.svg" => include_bytes!("../../assets/icons/circle-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"),
+25 -10
View File
@@ -684,7 +684,7 @@ impl Tty7App {
// active it also carries a trailing `(N)` count of that section's matching
// settings — the full section nav stays put and is annotated with
// per-section hit counts, rather than collapsing into a flat result list.
let nav_item = |label: &'static str, target: SettingsSection, icon: IconName| {
let nav_item = |label: &'static str, target: SettingsSection, icon: Icon| {
let view = cx.entity();
let count = if query.is_empty() {
0
@@ -692,7 +692,7 @@ impl Tty7App {
section_match_count(target, &query)
};
let item = SidebarMenuItem::new(label)
.icon(Icon::new(icon))
.icon(icon)
.active(section == target)
.on_click(move |_, _window, cx| {
view.update(cx, |this, cx| this.select_settings_section(target, cx));
@@ -715,7 +715,7 @@ impl Tty7App {
.child(nav_item(
"Appearance",
SettingsSection::Appearance,
IconName::Palette,
Icon::new(IconName::Palette),
))
// Sliders for Terminal (it's the tuning page), the `>_`
// prompt glyph for Shell (it configures the prompt's
@@ -723,28 +723,43 @@ impl Tty7App {
.child(nav_item(
"Terminal",
SettingsSection::Terminal,
IconName::Settings2,
Icon::new(IconName::Settings2),
))
.child(nav_item(
"Shell",
SettingsSection::Shell,
IconName::SquareTerminal,
Icon::new(IconName::SquareTerminal),
))
.child(nav_item(
"SSH",
SettingsSection::Ssh,
Icon::new(IconName::Globe),
))
.child(nav_item(
"Agents",
SettingsSection::Agents,
Icon::new(IconName::Bot),
))
.child(nav_item("SSH", SettingsSection::Ssh, IconName::Globe))
.child(nav_item("Agents", SettingsSection::Agents, IconName::Bot))
.child(nav_item(
"Window & Tabs",
SettingsSection::WindowTabs,
IconName::WindowRestore,
Icon::new(IconName::WindowRestore),
))
// The icon set ships no keyboard glyph; CaseSensitive ("Aa")
// is the closest key-ish cue available.
.child(nav_item(
"Keybindings",
SettingsSection::Keybindings,
IconName::CaseSensitive,
Icon::new(IconName::CaseSensitive),
))
.child(nav_item("About", SettingsSection::About, IconName::Info));
// Not `IconName::Info`: `icons/info.svg` is overridden app-wide with
// the detail panel's "panel with two lines" glyph, which reads as a
// document, not as *About*. This row keeps the circled `i`.
.child(nav_item(
"About",
SettingsSection::About,
Icon::empty().path("icons/circle-info.svg"),
));
let sidebar = Sidebar::new("settings-sidebar")
.collapsible(SidebarCollapsible::None)