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,