diff --git a/assets/icons/circle-info.svg b/assets/icons/circle-info.svg
new file mode 100644
index 00000000..ec30e25e
--- /dev/null
+++ b/assets/icons/circle-info.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
new file mode 100644
index 00000000..55af1b1f
--- /dev/null
+++ b/assets/icons/copy.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg
new file mode 100644
index 00000000..2cbbc2be
--- /dev/null
+++ b/assets/icons/ellipsis.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg
new file mode 100644
index 00000000..6b0f0238
--- /dev/null
+++ b/assets/icons/eye.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/file.svg b/assets/icons/file.svg
new file mode 100644
index 00000000..fab17eac
--- /dev/null
+++ b/assets/icons/file.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/folder-closed.svg b/assets/icons/folder-closed.svg
new file mode 100644
index 00000000..be3c5989
--- /dev/null
+++ b/assets/icons/folder-closed.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/folder-open.svg b/assets/icons/folder-open.svg
new file mode 100644
index 00000000..2c0a9527
--- /dev/null
+++ b/assets/icons/folder-open.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg
new file mode 100644
index 00000000..be3c5989
--- /dev/null
+++ b/assets/icons/folder.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/git-branch.svg b/assets/icons/git-branch.svg
index b809d259..c85b0a16 100644
--- a/assets/icons/git-branch.svg
+++ b/assets/icons/git-branch.svg
@@ -1 +1 @@
-
+
diff --git a/assets/icons/info.svg b/assets/icons/info.svg
index ec30e25e..2f5b329c 100644
--- a/assets/icons/info.svg
+++ b/assets/icons/info.svg
@@ -1 +1 @@
-
+
diff --git a/assets/icons/list.svg b/assets/icons/list.svg
index 981d0c33..f840375b 100644
--- a/assets/icons/list.svg
+++ b/assets/icons/list.svg
@@ -1 +1 @@
-
+
diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg
new file mode 100644
index 00000000..b9457a10
--- /dev/null
+++ b/assets/icons/plus.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/search.svg b/assets/icons/search.svg
new file mode 100644
index 00000000..4fcdc073
--- /dev/null
+++ b/assets/icons/search.svg
@@ -0,0 +1 @@
+
diff --git a/src/ui/assets.rs b/src/ui/assets.rs
index af8c1cd9..0122bb34 100644
--- a/src/ui/assets.rs
+++ b/src/ui/assets.rs
@@ -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.6–3.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"),
diff --git a/src/ui/settings.rs b/src/ui/settings.rs
index abc9de61..8ad3ffee 100644
--- a/src/ui/settings.rs
+++ b/src/ui/settings.rs
@@ -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)