From d60d92e909bcde3df59e380e73cafc4134f7af1e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 16 Aug 2026 11:46:15 +0200 Subject: [PATCH] feat(app): show workspace identity badges (#3445) Let users opt into branch and project context on workspace rows while keeping the default sidebar quiet. Project names follow the existing project-icon visibility rule so grouped rows do not repeat their parent. --- .../sidebar/display-preferences/menu.tsx | 4 ++ .../display-preferences/row-items.test.ts | 15 +++--- .../sidebar/display-preferences/row-items.ts | 16 ++++--- .../sidebar/sidebar-workspace-row-content.tsx | 2 + .../sidebar/workspace-meta-row/index.tsx | 47 +++++++++++++++++++ .../workspace-meta-row/meta-items.test.ts | 36 ++++++++++++-- .../sidebar/workspace-meta-row/meta-items.ts | 20 +++++++- .../app/src/hooks/use-settings/storage.ts | 2 + packages/app/src/i18n/resources/ar.ts | 2 + packages/app/src/i18n/resources/en.ts | 2 + packages/app/src/i18n/resources/es.ts | 2 + packages/app/src/i18n/resources/fr.ts | 2 + packages/app/src/i18n/resources/ja.ts | 2 + packages/app/src/i18n/resources/ko.ts | 2 + packages/app/src/i18n/resources/pt-BR.ts | 2 + packages/app/src/i18n/resources/ru.ts | 2 + packages/app/src/i18n/resources/zh-CN.ts | 2 + .../settings/host-appearance-section.tsx | 8 +++- 18 files changed, 151 insertions(+), 17 deletions(-) diff --git a/packages/app/src/components/sidebar/display-preferences/menu.tsx b/packages/app/src/components/sidebar/display-preferences/menu.tsx index b0ba9ba52..b9820b0d4 100644 --- a/packages/app/src/components/sidebar/display-preferences/menu.tsx +++ b/packages/app/src/components/sidebar/display-preferences/menu.tsx @@ -66,6 +66,8 @@ const TITLE_SOURCE_ICONS: Record = { // The same marks these things carry on the workspace row itself, so the menu and the row it // configures name each item the same way twice. const ROW_ITEM_ICONS: Record = { + branch: withUnistyles(GitBranch), + project: withUnistyles(Folder), host: withUnistyles(Server), changeRequest: withUnistyles(GitPullRequest), services: withUnistyles(Globe), @@ -99,6 +101,8 @@ const TITLE_SOURCE_LABEL_KEYS: Record = { }; const ROW_ITEM_LABEL_KEYS: Record = { + branch: "sidebar.display.show.branch", + project: "sidebar.display.show.project", host: "sidebar.display.show.host", changeRequest: "sidebar.display.show.changeRequest", services: "sidebar.display.show.services", diff --git a/packages/app/src/components/sidebar/display-preferences/row-items.test.ts b/packages/app/src/components/sidebar/display-preferences/row-items.test.ts index c83c4a705..4f1b3ef9f 100644 --- a/packages/app/src/components/sidebar/display-preferences/row-items.test.ts +++ b/packages/app/src/components/sidebar/display-preferences/row-items.test.ts @@ -3,14 +3,17 @@ import { DEFAULT_SIDEBAR_ROW_ITEMS, isChecksHiddenByLegacyRowItem, parseSidebarRowItems, - SIDEBAR_ROW_ITEMS, } from "./row-items"; describe("parseSidebarRowItems", () => { - it("shows everything by default", () => { - for (const item of SIDEBAR_ROW_ITEMS) { - expect(DEFAULT_SIDEBAR_ROW_ITEMS[item]).toBe(true); - } + it("shows operational metadata but hides identity badges by default", () => { + expect(DEFAULT_SIDEBAR_ROW_ITEMS).toEqual({ + branch: false, + project: false, + host: true, + changeRequest: true, + services: true, + }); }); it("applies stored overrides", () => { @@ -21,7 +24,7 @@ describe("parseSidebarRowItems", () => { }); it("leaves items absent from storage at their default", () => { - // A newly added item must ship visible rather than inheriting "missing means off". + // An absent key takes the item's explicit product default. expect(parseSidebarRowItems({ host: false })).toEqual({ ...DEFAULT_SIDEBAR_ROW_ITEMS, host: false, diff --git a/packages/app/src/components/sidebar/display-preferences/row-items.ts b/packages/app/src/components/sidebar/display-preferences/row-items.ts index 3ef1d04aa..f577e18fd 100644 --- a/packages/app/src/components/sidebar/display-preferences/row-items.ts +++ b/packages/app/src/components/sidebar/display-preferences/row-items.ts @@ -12,18 +12,22 @@ * that knows what an item is. */ -export const SIDEBAR_ROW_ITEMS = ["host", "changeRequest", "services"] as const; +export const SIDEBAR_ROW_ITEMS = [ + "branch", + "project", + "host", + "changeRequest", + "services", +] as const; export type SidebarRowItem = (typeof SIDEBAR_ROW_ITEMS)[number]; export type SidebarRowItems = Record; -/** - * Everything on by default. A new item ships visible and users turn it off, which is also why - * the persisted shape is a record of overrides rather than a list of enabled items: an absent - * key has to mean "default", not "off". - */ +/** The persisted record is merged over these explicit product defaults. */ export const DEFAULT_SIDEBAR_ROW_ITEMS: SidebarRowItems = { + branch: false, + project: false, host: true, changeRequest: true, services: true, diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx index 2e6b5ac5e..a73609b3e 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx @@ -196,6 +196,8 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte {children} ({ color: theme.colors.foreground }); +const mutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const mergedMapping = (theme: Theme) => ({ color: theme.colors.statusMerged }); const dangerMapping = (theme: Theme) => ({ color: theme.colors.statusDanger }); @@ -55,16 +60,22 @@ const dangerMapping = (theme: Theme) => ({ color: theme.colors.statusDanger }); * leaves color to mean status. */ export function WorkspaceMetaRow({ + currentBranch, + projectName, hostBadge, prHint, serviceSummary, }: { + currentBranch: string | null; + projectName: string | null; hostBadge: HostBadgeModel | null; prHint: PrHint | null; serviceSummary: WorkspaceServiceSummary | null; }) { const { rowItems, checksDisplay } = useSidebarMetaPreferences(); const items = selectMetaRowItems({ + currentBranch, + projectName, hasHostBadge: hostBadge !== null, prHint, serviceSummary, @@ -93,6 +104,12 @@ function MetaItemNode({ item: MetaRowItem; hostBadge: HostBadgeModel | null; }): ReactNode { + if (item.kind === "branch") { + return ; + } + if (item.kind === "project") { + return ; + } if (item.kind === "host") { return hostBadge ? : null; } @@ -105,6 +122,20 @@ function MetaItemNode({ return ; } +function IdentityItem({ kind, name }: { kind: "branch" | "project"; name: string }) { + const Icon = kind === "branch" ? ThemedGitBranch : ThemedFolder; + return ( + + + + + + {name} + + + ); +} + /** * The only thing on the line that navigates, so it is the only thing that answers the * pointer: it brightens to full foreground and its icon becomes an external-link arrow, @@ -267,6 +298,22 @@ const styles = StyleSheet.create((theme) => ({ minWidth: 0, flexShrink: 0, }, + identityItem: { + flexDirection: "row", + alignItems: "center", + gap: 3, + minWidth: 0, + flexShrink: 1, + }, + identityIcon: { + flexShrink: 0, + }, + identityText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + lineHeight: 16, + flexShrink: 1, + }, itemPressed: { opacity: 0.82, }, diff --git a/packages/app/src/components/sidebar/workspace-meta-row/meta-items.test.ts b/packages/app/src/components/sidebar/workspace-meta-row/meta-items.test.ts index fc3c424fa..9cf097869 100644 --- a/packages/app/src/components/sidebar/workspace-meta-row/meta-items.test.ts +++ b/packages/app/src/components/sidebar/workspace-meta-row/meta-items.test.ts @@ -17,6 +17,8 @@ const SERVICE: WorkspaceServiceSummary = { name: "web", health: null }; function select(overrides: Partial[0]> = {}) { return selectMetaRowItems({ + currentBranch: "feature/sidebar-badges", + projectName: "Paseo", hasHostBadge: true, prHint: PR_HINT, serviceSummary: SERVICE, @@ -29,12 +31,40 @@ function select(overrides: Partial[0]> = { const kinds = (items: ReturnType) => items.map((item) => item.kind); describe("selectMetaRowItems", () => { - it("reads identity, then the change, then its state, then what is running", () => { - expect(kinds(select())).toEqual(["host", "changeRequest", "checks", "services"]); + it("puts the enabled branch and project badges first", () => { + const visible = { ...DEFAULT_SIDEBAR_ROW_ITEMS, branch: true, project: true }; + expect(kinds(select({ visible }))).toEqual([ + "branch", + "project", + "host", + "changeRequest", + "checks", + "services", + ]); }); it("omits what the workspace does not have", () => { - expect(kinds(select({ hasHostBadge: false, prHint: null, serviceSummary: null }))).toEqual([]); + expect( + kinds( + select({ + currentBranch: null, + projectName: null, + hasHostBadge: false, + prHint: null, + serviceSummary: null, + }), + ), + ).toEqual([]); + }); + + it("only draws identity badges when enabled and available", () => { + const visible = { ...DEFAULT_SIDEBAR_ROW_ITEMS, branch: true, project: true }; + expect(kinds(select({ currentBranch: null, projectName: null, visible }))).toEqual([ + "host", + "changeRequest", + "checks", + "services", + ]); }); it.each([ diff --git a/packages/app/src/components/sidebar/workspace-meta-row/meta-items.ts b/packages/app/src/components/sidebar/workspace-meta-row/meta-items.ts index 98a52fcfc..f15bde443 100644 --- a/packages/app/src/components/sidebar/workspace-meta-row/meta-items.ts +++ b/packages/app/src/components/sidebar/workspace-meta-row/meta-items.ts @@ -10,6 +10,8 @@ import type { WorkspaceServiceSummary } from "./service-summary"; * running. Identity first, then the work, then the work's state. */ export type MetaRowItem = + | { kind: "branch"; name: string } + | { kind: "project"; name: string } | { kind: "host" } | { kind: "changeRequest"; hint: PrHint } | { kind: "checks"; summary: CheckSummary; label: boolean } @@ -26,15 +28,31 @@ export type MetaRowItem = * has no badge to hand down, so by the time a row sees one it is meant to be drawn. */ export function selectMetaRowItems(input: { + currentBranch: string | null; + projectName: string | null; hasHostBadge: boolean; prHint: PrHint | null; serviceSummary: WorkspaceServiceSummary | null; visible: SidebarRowItems; checksDisplay: SidebarChecksDisplay; }): MetaRowItem[] { - const { hasHostBadge, prHint, serviceSummary, visible, checksDisplay } = input; + const { + currentBranch, + projectName, + hasHostBadge, + prHint, + serviceSummary, + visible, + checksDisplay, + } = input; const items: MetaRowItem[] = []; + if (currentBranch && visible.branch) { + items.push({ kind: "branch", name: currentBranch }); + } + if (projectName && visible.project) { + items.push({ kind: "project", name: projectName }); + } if (hasHostBadge) { items.push({ kind: "host" }); } diff --git a/packages/app/src/hooks/use-settings/storage.ts b/packages/app/src/hooks/use-settings/storage.ts index 0b7a733de..7c606377f 100644 --- a/packages/app/src/hooks/use-settings/storage.ts +++ b/packages/app/src/hooks/use-settings/storage.ts @@ -78,6 +78,8 @@ export interface Settings extends AppSettings { } const SidebarRowItemsSchema = z.strictObject({ + branch: z.boolean().optional(), + project: z.boolean().optional(), host: z.boolean().optional(), changeRequest: z.boolean().optional(), services: z.boolean().optional(), diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index a864715ed..003ecd92c 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -970,6 +970,8 @@ export const ar: TranslationResources = { }, show: { label: "إظهار", + branch: "الفرع", + project: "المشروع", host: "المضيف", changeRequest: "طلب السحب", checks: "الفحوصات", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index cc01b209b..b84c68d61 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -979,6 +979,8 @@ export const en = { }, show: { label: "Show", + branch: "Branch", + project: "Project", host: "Host", changeRequest: "Pull request", checks: "Checks", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 2e00d8fb7..09f9eff74 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1001,6 +1001,8 @@ export const es: TranslationResources = { }, show: { label: "Mostrar", + branch: "Rama", + project: "Proyecto", host: "Host", changeRequest: "Pull request", checks: "Comprobaciones", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index bb2154e87..754d2c67e 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1000,6 +1000,8 @@ export const fr: TranslationResources = { }, show: { label: "Afficher", + branch: "Branche", + project: "Projet", host: "Hôte", changeRequest: "Pull request", checks: "Vérifications", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 62e46e8dd..88695d494 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -981,6 +981,8 @@ export const ja: TranslationResources = { }, show: { label: "表示項目", + branch: "ブランチ", + project: "プロジェクト", host: "ホスト", changeRequest: "プルリクエスト", checks: "チェック", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index 0f3652207..334c4ef92 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -977,6 +977,8 @@ export const ko: TranslationResources = { }, show: { label: "표시 항목", + branch: "브랜치", + project: "프로젝트", host: "호스트", changeRequest: "풀 리퀘스트", checks: "검사", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index bbec62c18..1f5a8e27d 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -992,6 +992,8 @@ export const ptBR: TranslationResources = { }, show: { label: "Mostrar", + branch: "Branch", + project: "Projeto", host: "Host", changeRequest: "Pull request", checks: "Verificações", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 7e6f3de80..2fdaebf7b 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -992,6 +992,8 @@ export const ru: TranslationResources = { }, show: { label: "Показывать", + branch: "Ветка", + project: "Проект", host: "Хост", changeRequest: "Pull request", checks: "Проверки", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 02c479538..381f6ec45 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -962,6 +962,8 @@ export const zhCN: TranslationResources = { }, show: { label: "显示", + branch: "分支", + project: "项目", host: "主机", changeRequest: "拉取请求", checks: "检查", diff --git a/packages/app/src/screens/settings/host-appearance-section.tsx b/packages/app/src/screens/settings/host-appearance-section.tsx index 448285cb4..50f172582 100644 --- a/packages/app/src/screens/settings/host-appearance-section.tsx +++ b/packages/app/src/screens/settings/host-appearance-section.tsx @@ -241,7 +241,13 @@ function BadgePreview({ {t("settings.host.appearance.preview.workspaceName")} - + ); }