diff --git a/apps/desktop/src/renderer/src/components/tab-bar.test.tsx b/apps/desktop/src/renderer/src/components/tab-bar.test.tsx
index 9938af3f7..f1b59d026 100644
--- a/apps/desktop/src/renderer/src/components/tab-bar.test.tsx
+++ b/apps/desktop/src/renderer/src/components/tab-bar.test.tsx
@@ -131,4 +131,21 @@ describe("TabBar hover action buttons", () => {
const pinnedTab = getByLabelText("Issues (pinned)");
expect(within(pinnedTab).getByText("Issues")).toBeTruthy();
});
+
+ it("renders the Pin glyph as the leading icon on a pinned tab and the route icon on an unpinned tab", () => {
+ state.byWorkspace.acme.tabs = [
+ { id: "tA", path: "/acme/issues", title: "Issues", icon: "ListTodo", pinned: true },
+ { id: "tB", path: "/acme/projects", title: "Projects", icon: "ListTodo", pinned: false },
+ ];
+ const { getByLabelText } = render();
+ const pinnedTab = getByLabelText("Issues (pinned)");
+ const unpinnedTab = getByLabelText("Projects");
+ // lucide-react renders the icon name into the class list. The leading
+ // slot icon is size-3.5; the hover Pin/Unpin action button is size-2.5,
+ // so we qualify on size to avoid matching the action glyph.
+ expect(pinnedTab.querySelector(".lucide-pin.size-3\\.5")).toBeTruthy();
+ expect(pinnedTab.querySelector(".lucide-list-todo")).toBeNull();
+ expect(unpinnedTab.querySelector(".lucide-list-todo.size-3\\.5")).toBeTruthy();
+ expect(unpinnedTab.querySelector(".lucide-pin.size-3\\.5")).toBeNull();
+ });
});
diff --git a/apps/desktop/src/renderer/src/components/tab-bar.tsx b/apps/desktop/src/renderer/src/components/tab-bar.tsx
index ff033dffc..35f4e5c78 100644
--- a/apps/desktop/src/renderer/src/components/tab-bar.tsx
+++ b/apps/desktop/src/renderer/src/components/tab-bar.tsx
@@ -84,7 +84,11 @@ function SortableTabItem({
isDragging,
} = useSortable({ id: tab.id });
- const Icon = TAB_ICONS[tab.icon];
+ // Pinned tabs swap the route icon for a Pin glyph as the static "I am
+ // pinned" indicator (RFC §3 D1v-iv FINAL). The route information is still
+ // present in the title, and this avoids a hard left accent border that read
+ // as visually heavy in light mode.
+ const LeadingIcon = tab.pinned ? Pin : TAB_ICONS[tab.icon];
const style = {
transform: CSS.Transform.toString(transform),
@@ -112,10 +116,10 @@ function SortableTabItem({
e.stopPropagation();
};
- // Pinned tabs keep their full title (RFC §3 D1v-ii FINAL). The only weak
- // visual differences vs. unpinned tabs are the accent left border and the
- // suppressed X (closing requires explicit Unpin). Pin/Unpin is reachable
- // via the hover action button below and the right-click menu fallback.
+ // Pinned tabs keep their full title (RFC §3 D1v-ii FINAL). The only visual
+ // differences vs. unpinned tabs are the leading Pin icon (swapped in above)
+ // and the suppressed X (closing requires explicit Unpin). Pin/Unpin is
+ // reachable via the hover action button below and the right-click menu.
const showCloseButton = !tab.pinned && !isOnly;
const tabButton = (
@@ -133,11 +137,10 @@ function SortableTabItem({
isActive
? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
: "bg-sidebar-accent/50 text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
- tab.pinned && "border-l-2 border-l-primary/60",
isDragging && "opacity-60",
)}
>
- {Icon && }
+ {LeadingIcon && }