mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 00:26:28 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The task detail page shows a breadcrumb header with the task status glyph and the task title. > - The breadcrumb did not show the task identifier, so a reader could not name the task without opening extra context. > - Agents and people refer to tasks by identifier, so the identifier belongs next to the title. > - This pull request renders the task identifier in the breadcrumb header, between the status glyph and the title. > - The benefit is faster reference: a reader sees the task key and the title together at the top of the page. ## Linked Issues or Issue Description **What existing behavior does this improve?** The task detail breadcrumb header. It shows the status glyph and the task title, but not the task identifier. **Subsystem affected** Web UI — the breadcrumb bar on the task detail page (`ui/src/components/BreadcrumbBar.tsx`, `ui/src/context/BreadcrumbContext.tsx`, `ui/src/pages/IssueDetail.tsx`). **Current behavior** The breadcrumb header renders the status glyph and then the task title. The task identifier does not appear in the header. **Proposed behavior** The breadcrumb header renders the task identifier between the status glyph and the title. The identifier uses gray monospace styling from design tokens (`font-mono text-muted-foreground`). **Reason and benefit** A reader can name and reference the task from the header without opening more context. The identifier and the title appear together. **Breaking changes** None. The identifier field is optional. Crumbs without an identifier render as before. ## What Changed - Add an optional `identifier` field to the `Breadcrumb` type and include it in the `breadcrumbsEqual` comparison so an identifier change triggers a fresh render. - Add a `CrumbIdentifier` helper in `BreadcrumbBar` that renders the identifier in gray monospace (`font-mono text-muted-foreground`), placed after the leading status glyph in each crumb variant. - Wire the issue identifier onto the task crumb in `IssueDetail`. - Add unit tests that cover the identifier field in `breadcrumbsEqual` (fresh render on change, no-op on identical value). ## Verification - `pnpm check:token-gates` → 3/3 gates CLEAN (color literals, arbitrary bracket values, raw font-size). - `pnpm --filter @paperclipai/ui exec vitest run src/context/BreadcrumbContext.test.tsx` → 4/4 tests pass. - `pnpm typecheck` → the four changed files typecheck clean. - Manual: open a task detail page. The breadcrumb header shows the status glyph, then the task identifier in gray monospace, then the title. Visual change. Snapshot baselines are intentionally not updated, per `doc/design/DECISION-SHEET.md` → "Per-change snapshot verification demoted to dormant (Jul 13 2026)". ## Risks Low risk. The change is additive and the identifier field is optional. It touches only the breadcrumb header rendering and the equality check. No data model or API change. ## Model Used Claude Opus 4.8 (Anthropic), model id `claude-opus-4-8`, extended thinking enabled, tool use enabled. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
3.5 KiB
TypeScript
123 lines
3.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { BreadcrumbProvider, buildDocumentTitle, useBreadcrumbs } from "./BreadcrumbContext";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
describe("BreadcrumbContext", () => {
|
|
let container: HTMLDivElement;
|
|
let root: ReturnType<typeof createRoot>;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
});
|
|
|
|
it("does not rerender consumers when breadcrumbs are set to the same values", () => {
|
|
const renderCounts: number[] = [];
|
|
let updateBreadcrumbs: ((crumbs: Array<{ label: string; href?: string }>) => void) | null = null;
|
|
|
|
function TestConsumer() {
|
|
const { breadcrumbs, setBreadcrumbs } = useBreadcrumbs();
|
|
renderCounts.push(breadcrumbs.length);
|
|
updateBreadcrumbs = setBreadcrumbs;
|
|
return null;
|
|
}
|
|
|
|
act(() => {
|
|
root.render(
|
|
<BreadcrumbProvider>
|
|
<TestConsumer />
|
|
</BreadcrumbProvider>,
|
|
);
|
|
});
|
|
|
|
expect(renderCounts).toHaveLength(1);
|
|
|
|
act(() => {
|
|
updateBreadcrumbs?.([{ label: "Issues", href: "/issues" }, { label: "PAP-1488" }]);
|
|
});
|
|
|
|
expect(renderCounts).toHaveLength(2);
|
|
|
|
act(() => {
|
|
updateBreadcrumbs?.([{ label: "Issues", href: "/issues" }, { label: "PAP-1488" }]);
|
|
});
|
|
|
|
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(
|
|
<BreadcrumbProvider>
|
|
<TestConsumer />
|
|
</BreadcrumbProvider>,
|
|
);
|
|
});
|
|
|
|
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",
|
|
);
|
|
expect(
|
|
buildDocumentTitle(
|
|
[{ label: "Issues", href: "/issues" }, { label: "PAP-3515" }],
|
|
"Anachronist Wiki",
|
|
),
|
|
).toBe("PAP-3515 • Issues • Anachronist Wiki • Paperclip");
|
|
});
|
|
|
|
it("omits blank company names from page titles", () => {
|
|
expect(buildDocumentTitle([{ label: "Inbox" }], " ")).toBe("Inbox • Paperclip");
|
|
expect(buildDocumentTitle([], null)).toBe("Paperclip");
|
|
});
|
|
});
|