diff --git a/ui/src/components/BreadcrumbBar.tsx b/ui/src/components/BreadcrumbBar.tsx index cde8220b44..3bb3407efe 100644 --- a/ui/src/components/BreadcrumbBar.tsx +++ b/ui/src/components/BreadcrumbBar.tsx @@ -18,6 +18,12 @@ import { PluginLauncherOutlet, usePluginLaunchers } from "@/plugins/launchers"; type GlobalToolbarContext = { companyId: string | null; companyPrefix: string | null }; +/** Task identifier rendered in gray monospace between the glyph and the title. */ +function CrumbIdentifier({ identifier }: { identifier?: string }) { + if (!identifier) return null; + return {identifier}; +} + function GlobalToolbar({ context }: { context: GlobalToolbarContext }) { const { slots } = usePluginSlots({ slotTypes: ["globalToolbarButton"], companyId: context.companyId }); const { launchers } = usePluginLaunchers({ placementZones: ["globalToolbarButton"], companyId: context.companyId, enabled: !!context.companyId }); @@ -82,9 +88,12 @@ export function BreadcrumbBar() {
{menuButton}
- {breadcrumbs[0].leading ? ( + {breadcrumbs[0].leading || breadcrumbs[0].identifier ? (

- {breadcrumbs[0].leading} + {breadcrumbs[0].leading && ( + {breadcrumbs[0].leading} + )} + {breadcrumbs[0].label}

) : ( @@ -112,9 +121,12 @@ export function BreadcrumbBar() { {i > 0 && } {isLast || !crumb.href ? ( - crumb.leading ? ( + crumb.leading || crumb.identifier ? ( - {crumb.leading} + {crumb.leading && ( + {crumb.leading} + )} + {crumb.label} ) : ( @@ -122,9 +134,12 @@ export function BreadcrumbBar() { ) ) : ( - {crumb.leading ? ( + {crumb.leading || crumb.identifier ? ( - {crumb.leading} + {crumb.leading && ( + {crumb.leading} + )} + {crumb.label} ) : ( diff --git a/ui/src/context/BreadcrumbContext.test.tsx b/ui/src/context/BreadcrumbContext.test.tsx index ab2eb67b50..19516c92c4 100644 --- a/ui/src/context/BreadcrumbContext.test.tsx +++ b/ui/src/context/BreadcrumbContext.test.tsx @@ -59,6 +59,50 @@ describe("BreadcrumbContext", () => { expect(renderCounts).toHaveLength(2); }); + it("rerenders consumers when only the crumb identifier changes", () => { + const renderCounts: number[] = []; + let updateBreadcrumbs: + | ((crumbs: Array<{ label: string; href?: string; identifier?: string }>) => void) + | null = null; + + function TestConsumer() { + const { breadcrumbs, setBreadcrumbs } = useBreadcrumbs(); + renderCounts.push(breadcrumbs.length); + updateBreadcrumbs = setBreadcrumbs; + return null; + } + + act(() => { + root.render( + + + , + ); + }); + + expect(renderCounts).toHaveLength(1); + + act(() => { + updateBreadcrumbs?.([{ label: "First task prompt", identifier: "PAP-1204" }]); + }); + + expect(renderCounts).toHaveLength(2); + + // Same everything but a new identifier must produce a fresh render. + act(() => { + updateBreadcrumbs?.([{ label: "First task prompt", identifier: "PAP-1205" }]); + }); + + expect(renderCounts).toHaveLength(3); + + // Identical identifier is a no-op. + act(() => { + updateBreadcrumbs?.([{ label: "First task prompt", identifier: "PAP-1205" }]); + }); + + expect(renderCounts).toHaveLength(3); + }); + it("builds page titles with the selected company name before Paperclip", () => { expect(buildDocumentTitle([{ label: "Inbox" }], "Anachronist Wiki")).toBe( "Inbox • Anachronist Wiki • Paperclip", diff --git a/ui/src/context/BreadcrumbContext.tsx b/ui/src/context/BreadcrumbContext.tsx index 5c7fc43161..2726c9a904 100644 --- a/ui/src/context/BreadcrumbContext.tsx +++ b/ui/src/context/BreadcrumbContext.tsx @@ -3,6 +3,11 @@ import { createContext, useCallback, useContext, useEffect, useState, type React export interface Breadcrumb { label: string; href?: string; + /** + * Optional task identifier (e.g. "PAP-1204") rendered in gray monospace + * between the leading glyph and the label. + */ + identifier?: string; /** Optional node rendered before the label (e.g. a status glyph). */ leading?: ReactNode; /** @@ -34,6 +39,7 @@ function breadcrumbsEqual(left: Breadcrumb[], right: Breadcrumb[]) { if ( left[index]?.label !== right[index]?.label || left[index]?.href !== right[index]?.href + || left[index]?.identifier !== right[index]?.identifier || left[index]?.leadingKey !== right[index]?.leadingKey ) { return false; diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 27c674bf2d..483fb2860d 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -2042,6 +2042,7 @@ export function IssueDetail() { [comments, optimisticComments], ); const breadcrumbTitle = issue?.title ?? issueId ?? "Task"; + const breadcrumbIdentifier = issue?.identifier ?? issueHeaderSeed?.identifier ?? undefined; const breadcrumbStatus = issue?.status; const breadcrumbBlockerAttention = issue?.blockerAttention; // Stable identity for the breadcrumb status glyph. The glyph's shape/colour @@ -3209,12 +3210,14 @@ export function IssueDetail() { // The status glyph (leading) already conveys in-progress/live state; // no redundant 🔵 emoji prefix on the title. label: breadcrumbTitle, + identifier: breadcrumbIdentifier, leading: breadcrumbStatusLeading, leadingKey: breadcrumbStatusKey, }, ]); }, [ breadcrumbTitle, + breadcrumbIdentifier, hasLiveRuns, setBreadcrumbs, sourceBreadcrumb.href,