+
- {presentation.label}
+ {props.decorative ? null : (
+ {presentation.label}
+ )}
);
}
diff --git a/packages/junior-dashboard/src/client/conversations/ConversationSidebar.tsx b/packages/junior-dashboard/src/client/conversations/ConversationSidebar.tsx
index 26cdf7f3de..1719fd7893 100644
--- a/packages/junior-dashboard/src/client/conversations/ConversationSidebar.tsx
+++ b/packages/junior-dashboard/src/client/conversations/ConversationSidebar.tsx
@@ -190,10 +190,14 @@ function ConversationSidebarRow(props: {
includeId: false,
});
const title = conversationDisplayTitle(props.conversation);
+ const hasAnnotations = Boolean(props.conversation.sidebarAnnotations?.length);
+ // Linked work is denser and more actionable than channel; hide channel when
+ // annotations own the meta row.
+ const showLocation = Boolean(location) && !hasAnnotations;
const hasMeta =
- Boolean(location) ||
+ showLocation ||
props.conversation.visibility === "private" ||
- Boolean(props.conversation.sidebarAnnotations?.length);
+ hasAnnotations;
return (
) : null}
- {location ? {location} : null}
- {location && props.conversation.sidebarAnnotations?.length ? (
-
- ·
-
- ) : null}
+ {showLocation ? {location} : null}
diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts
index 40ff7a1063..3fe9bbbc73 100644
--- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts
+++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts
@@ -133,7 +133,13 @@ function activeConversation(nowMs: number): ConversationDetailReport {
assignedWork: true,
unfinishedWork: true,
isPriority: true,
- sidebarAnnotations: [{ icon: "git-pull-request", key: "github", label: "payments" }],
+ sidebarAnnotations: [
+ {
+ icon: "git-pull-request",
+ key: "getsentry/payments#42",
+ label: "payments",
+ },
+ ],
annotations: [
{
kind: "resource_link",
@@ -331,7 +337,18 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
assignedWork: true,
unfinishedWork: true,
isPriority: true,
- sidebarAnnotations: [{ icon: "git-pull-request", key: "github", label: "junior" }],
+ sidebarAnnotations: [
+ {
+ icon: "git-pull-request",
+ key: "getsentry/junior#1081",
+ label: "junior",
+ },
+ {
+ icon: "circle-dot",
+ key: "getsentry/junior#1090",
+ label: "junior",
+ },
+ ],
annotations: [
{
kind: "resource_link",
@@ -763,7 +780,23 @@ function longConversation(nowMs: number): ConversationDetailReport {
assignedWork: true,
unfinishedWork: true,
isPriority: true,
- sidebarAnnotations: [{ icon: "git-pull-request", key: "github", label: "2 repos" }],
+ sidebarAnnotations: [
+ {
+ icon: "circle-dashed",
+ key: "getsentry/junior#2201",
+ label: "junior",
+ },
+ {
+ icon: "git-pull-request",
+ key: "getsentry/payments#91",
+ label: "payments",
+ },
+ {
+ icon: "git-merge",
+ key: "getsentry/relay#44",
+ label: "relay",
+ },
+ ],
annotations: [
{
kind: "resource_link",
@@ -777,23 +810,23 @@ function longConversation(nowMs: number): ConversationDetailReport {
},
{
kind: "resource_link",
- key: "getsentry/payments#88",
- label: "getsentry/payments#88",
+ key: "getsentry/payments#91",
+ label: "getsentry/payments#91",
plugin: "github",
status: "open",
- url: "https://github.com/getsentry/payments/issues/88",
+ url: "https://github.com/getsentry/payments/pull/91",
createdAt: startedAt,
- updatedAt: iso(Date.parse(startedAt), 70_000),
+ updatedAt: iso(Date.parse(startedAt), 75_000),
},
{
kind: "resource_link",
- key: "getsentry/payments#91",
- label: "getsentry/payments#91",
+ key: "getsentry/relay#44",
+ label: "getsentry/relay#44",
plugin: "github",
- status: "open",
- url: "https://github.com/getsentry/payments/pull/91",
+ status: "merged",
+ url: "https://github.com/getsentry/relay/pull/44",
createdAt: startedAt,
- updatedAt: iso(Date.parse(startedAt), 75_000),
+ updatedAt: iso(Date.parse(startedAt), 70_000),
},
],
cumulativeDurationMs: 552_761,
@@ -839,7 +872,18 @@ function incidentConversation(nowMs: number): ConversationDetailReport {
// Finished links show the final annotation state in the sidebar.
assignedWork: true,
finishedWorkAt: iso(nowMs, -42 * 60_000),
- sidebarAnnotations: [{ icon: "git-merge", key: "github", label: "payments" }],
+ sidebarAnnotations: [
+ {
+ icon: "git-merge",
+ key: "getsentry/payments#77",
+ label: "payments",
+ },
+ {
+ icon: "circle-x",
+ key: "getsentry/payments#61",
+ label: "payments",
+ },
+ ],
annotations: [
{
kind: "resource_link",
@@ -1736,7 +1780,8 @@ export function readMockPeoplePluginReports(
id: "pull-requests-created",
type: "bar_chart",
title: "Pull requests opened",
- description: "Junior-owned pull requests opened for this person per day",
+ description:
+ "Junior-owned pull requests opened for this person per day",
timeRangeDays: [7, 30, 90],
series: [{ key: "created", label: "Opened" }],
categories: days,
diff --git a/packages/junior-dashboard/tests/dashboard-mock-routes.test.ts b/packages/junior-dashboard/tests/dashboard-mock-routes.test.ts
index c7449b85d9..0d62b924b9 100644
--- a/packages/junior-dashboard/tests/dashboard-mock-routes.test.ts
+++ b/packages/junior-dashboard/tests/dashboard-mock-routes.test.ts
@@ -262,7 +262,16 @@ describe("dashboard canonical-event mock routes", () => {
);
expect(dashboardQa.annotations).toHaveLength(2);
expect(dashboardQa.sidebarAnnotations).toEqual([
- { icon: "git-pull-request", key: "github", label: "junior" },
+ {
+ icon: "git-pull-request",
+ key: "getsentry/junior#1081",
+ label: "junior",
+ },
+ {
+ icon: "circle-dot",
+ key: "getsentry/junior#1090",
+ label: "junior",
+ },
]);
expect(dashboardQa.unfinishedWork).toBe(true);
diff --git a/packages/junior-dashboard/tests/telemetry-components.test.tsx b/packages/junior-dashboard/tests/telemetry-components.test.tsx
index 6b8990da5b..9cc0790c82 100644
--- a/packages/junior-dashboard/tests/telemetry-components.test.tsx
+++ b/packages/junior-dashboard/tests/telemetry-components.test.tsx
@@ -586,19 +586,94 @@ describe("dashboard canonical-event components", () => {
expect(html).toContain('title="Open pull request"');
});
- it("renders plugin-selected sidebar annotations", () => {
- const html = renderToStaticMarkup(
+ it("labels one or two scopes and clusters the rest on desktop", () => {
+ const single = renderToStaticMarkup(
+ ,
+ );
+ expect(single).toContain(">junior<");
+ expect(single).toContain("getsentry/junior#2");
+
+ const dual = renderToStaticMarkup(
+ ,
+ );
+ expect(dual).toContain(
+ 'aria-label="Linked work, newest first: getsentry/junior#2, getsentry/payments#1"',
+ );
+ expect(dual.indexOf(">junior<")).toBeLessThan(dual.indexOf(">payments<"));
+ expect(dual).not.toContain(">+1<");
+ // Chip icons are decorative; the parent aria-label carries the identity.
+ expect(dual).toContain("lucide-circle-dot");
+ expect(dual).toContain("lucide-git-merge");
+
+ const stacked = renderToStaticMarkup(
+ ,
+ );
+ expect(stacked).toContain(">junior<");
+ expect(stacked).toContain(">+2<");
+ expect(stacked).not.toContain(">payments<");
+ expect(stacked).not.toContain(">relay<");
+ expect(stacked).toContain("getsentry/payments#2");
+ expect(stacked).toContain("getsentry/relay#1");
+
+ const sameRepo = renderToStaticMarkup(
,
);
- expect(html).toContain("2 repos");
- expect(html).toContain("Merged");
- expect(html).toContain("Open pull request");
- expect(html).toContain("min-w-0 truncate whitespace-nowrap font-sans");
+ expect(sameRepo).toContain(">junior<");
+ expect(sameRepo.match(/>junior+1<");
+ expect(sameRepo).toContain("getsentry/junior#1");
});
it("distinguishes initial detail failures from stale refresh failures", () => {
diff --git a/packages/junior-github/src/annotations.ts b/packages/junior-github/src/annotations.ts
index e610223305..d47e24ef47 100644
--- a/packages/junior-github/src/annotations.ts
+++ b/packages/junior-github/src/annotations.ts
@@ -3,14 +3,6 @@ import type {
ConversationSidebarAnnotation,
} from "@sentry/junior-plugin-api";
-const STATUS_RANK = {
- warning: 5,
- open: 4,
- draft: 3,
- merged: 2,
- closed: 1,
-} as const;
-
const STATUS_ICON = {
warning: "triangle-alert",
open: "circle-dot",
@@ -19,18 +11,7 @@ const STATUS_ICON = {
closed: "circle-x",
} as const;
-type GitHubAnnotationStatus = keyof typeof STATUS_RANK;
-
-function repositoryScope(
- annotation: ConversationAnnotation,
-): { key: string; label: string } | undefined {
- try {
- const [, owner, repo] = new URL(annotation.url).pathname.split("/");
- return owner && repo ? { key: `${owner}/${repo}`, label: repo } : undefined;
- } catch {
- return undefined;
- }
-}
+type GitHubAnnotationStatus = keyof typeof STATUS_ICON;
function isPullRequestUrl(url: string): boolean {
try {
@@ -42,38 +23,44 @@ function isPullRequestUrl(url: string): boolean {
function sidebarIconForStatus(
status: GitHubAnnotationStatus,
- links: Array<{ isPullRequest: boolean; status: GitHubAnnotationStatus }>,
+ url: string,
): ConversationSidebarAnnotation["icon"] {
- if (
- status === "open" &&
- links.some((link) => link.status === "open" && link.isPullRequest)
- ) {
- return "git-pull-request";
- }
+ if (status === "open" && isPullRequestUrl(url)) return "git-pull-request";
return STATUS_ICON[status];
}
-/** Select the one GitHub annotation summary shown in a conversation row. */
-export function githubSidebarAnnotation(
+function repositoryName(
+ annotation: ConversationAnnotation,
+): string | undefined {
+ try {
+ const [, , repo] = new URL(annotation.url).pathname.split("/");
+ return repo || undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+/** Return GitHub annotations for a conversation row, newest first. */
+export function githubSidebarAnnotations(
annotations: ConversationAnnotation[],
-): ConversationSidebarAnnotation | undefined {
- const links = annotations.flatMap((annotation) => {
- const repo = repositoryScope(annotation);
- const status = annotation.status as GitHubAnnotationStatus | undefined;
- return repo && status
- ? [{ isPullRequest: isPullRequestUrl(annotation.url), repo, status }]
- : [];
- });
- if (links.length === 0) return undefined;
- const repos = new Map(links.map((link) => [link.repo.key, link.repo.label]));
- const status = links.reduce(
- (current, link) =>
- STATUS_RANK[link.status] > STATUS_RANK[current] ? link.status : current,
- "closed",
- );
- return {
- icon: sidebarIconForStatus(status, links),
- key: "github",
- label: repos.size === 1 ? [...repos.values()][0]! : `${repos.size} repos`,
- };
+): ConversationSidebarAnnotation[] {
+ return annotations
+ .flatMap((annotation) => {
+ const status = annotation.status as GitHubAnnotationStatus | undefined;
+ const label = repositoryName(annotation);
+ return status && label
+ ? [
+ {
+ annotation: {
+ icon: sidebarIconForStatus(status, annotation.url),
+ key: annotation.key,
+ label,
+ },
+ updatedAt: annotation.updatedAt,
+ },
+ ]
+ : [];
+ })
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
+ .map(({ annotation }) => annotation);
}
diff --git a/packages/junior-github/src/plugin.ts b/packages/junior-github/src/plugin.ts
index 0af6b8071e..e27f78741b 100644
--- a/packages/junior-github/src/plugin.ts
+++ b/packages/junior-github/src/plugin.ts
@@ -40,7 +40,7 @@ import type { GitHubDb } from "./db/database.js";
import { buildGitHubProfileReport } from "./outcomes/profile-report.js";
import { buildGitHubOutcomeReport } from "./outcomes/report.js";
import { classifyGitHubPullRequestCommitComposition } from "./pull-request-outcomes/commit-composition.js";
-import { githubSidebarAnnotation } from "./annotations.js";
+import { githubSidebarAnnotations } from "./annotations.js";
import {
listGitHubAssignedWork,
listGitHubFinishedWork,
@@ -742,10 +742,12 @@ export function githubPlugin(
return {
annotationsByConversationId: Object.fromEntries(
ctx.conversationIds.flatMap((conversationId) => {
- const annotation = githubSidebarAnnotation(
+ const annotations = githubSidebarAnnotations(
ctx.annotationsByConversationId[conversationId] ?? [],
);
- return annotation ? [[conversationId, [annotation]]] : [];
+ return annotations.length > 0
+ ? [[conversationId, annotations]]
+ : [];
}),
),
};
diff --git a/packages/junior-github/tests/annotations.test.ts b/packages/junior-github/tests/annotations.test.ts
index 9734fc0a53..08caef1f63 100644
--- a/packages/junior-github/tests/annotations.test.ts
+++ b/packages/junior-github/tests/annotations.test.ts
@@ -3,13 +3,14 @@ import {
type ConversationAnnotation,
} from "@sentry/junior-plugin-api";
import { describe, expect, it } from "vitest";
-import { githubSidebarAnnotation } from "../src/annotations";
+import { githubSidebarAnnotations } from "../src/annotations";
function annotation(
repo: string,
number: number,
status: NonNullable,
owner = "getsentry",
+ updatedAt = "2026-01-01T00:00:01.000Z",
kind: "pull" | "issues" = "pull",
): ConversationAnnotation {
return {
@@ -19,73 +20,89 @@ function annotation(
label: `${owner}/${repo}#${number}`,
plugin: "github",
status,
- updatedAt: "2026-01-01T00:00:01.000Z",
+ updatedAt,
url: `https://github.com/${owner}/${repo}/${kind}/${number}`,
};
}
describe("GitHub conversation sidebar", () => {
- it("selects repository scope and final status", () => {
- expect(
- githubSidebarAnnotation([
- annotation("junior", 1, "merged"),
- annotation("junior", 2, "closed"),
- ]),
- ).toEqual({ icon: "git-merge", key: "github", label: "junior" });
- expect(
- githubSidebarAnnotation([
- annotation("junior", 1, "merged"),
- annotation("payments", 2, "open"),
- ]),
- ).toEqual({ icon: "git-pull-request", key: "github", label: "2 repos" });
- expect(
- githubSidebarAnnotation([
- annotation("junior", 1, "closed"),
- annotation("junior", 2, "closed"),
- ]),
- ).toEqual({ icon: "circle-x", key: "github", label: "junior" });
+ it("returns every annotation newest first", () => {
+ const sidebar = githubSidebarAnnotations([
+ annotation(
+ "junior",
+ 1,
+ "merged",
+ "getsentry",
+ "2026-01-01T00:00:01.000Z",
+ ),
+ annotation(
+ "payments",
+ 2,
+ "open",
+ "getsentry",
+ "2026-01-01T00:00:02.000Z",
+ ),
+ annotation(
+ "junior",
+ 3,
+ "closed",
+ "getsentry",
+ "2026-01-01T00:00:03.000Z",
+ ),
+ ]);
+
+ expect(sidebar).toEqual([
+ {
+ icon: "circle-x",
+ key: "getsentry/junior#3",
+ label: "junior",
+ },
+ {
+ icon: "git-pull-request",
+ key: "getsentry/payments#2",
+ label: "payments",
+ },
+ {
+ icon: "git-merge",
+ key: "getsentry/junior#1",
+ label: "junior",
+ },
+ ]);
+ expect(() =>
+ conversationSidebarAnnotationSchema.array().parse(sidebar),
+ ).not.toThrow();
});
it("uses the pull request icon for open pull requests", () => {
- expect(githubSidebarAnnotation([annotation("junior", 1, "open")])).toEqual({
- icon: "git-pull-request",
- key: "github",
- label: "junior",
- });
+ expect(githubSidebarAnnotations([annotation("junior", 1, "open")])).toEqual(
+ [
+ {
+ icon: "git-pull-request",
+ key: "getsentry/junior#1",
+ label: "junior",
+ },
+ ],
+ );
});
it("keeps the issue icon for open issues", () => {
expect(
- githubSidebarAnnotation([annotation("junior", 1, "open", "getsentry", "issues")]),
- ).toEqual({ icon: "circle-dot", key: "github", label: "junior" });
- });
-
- it("prefers the pull request icon when open issues and pull requests mix", () => {
- expect(
- githubSidebarAnnotation([
- annotation("junior", 1, "open", "getsentry", "issues"),
- annotation("junior", 2, "open"),
- ]),
- ).toEqual({ icon: "git-pull-request", key: "github", label: "junior" });
- });
-
- it("counts repositories with the same name under different owners", () => {
- expect(
- githubSidebarAnnotation([
- annotation("shared", 1, "merged", "getsentry"),
- annotation("shared", 2, "open", "example"),
+ githubSidebarAnnotations([
+ annotation(
+ "junior",
+ 1,
+ "open",
+ "getsentry",
+ "2026-01-01T00:00:01.000Z",
+ "issues",
+ ),
]),
- ).toEqual({ icon: "git-pull-request", key: "github", label: "2 repos" });
- });
-
- it("keeps valid 100-character GitHub repository names", () => {
- const repo = "r".repeat(100);
- const sidebar = githubSidebarAnnotation([annotation(repo, 1, "open")]);
- expect(sidebar).toEqual({
- icon: "git-pull-request",
- key: "github",
- label: repo,
- });
- expect(() => conversationSidebarAnnotationSchema.parse(sidebar)).not.toThrow();
+ ).toEqual([
+ {
+ icon: "circle-dot",
+ key: "getsentry/junior#1",
+ label: "junior",
+ },
+ ]);
});
});
diff --git a/packages/junior-plugin-api/src/annotations.ts b/packages/junior-plugin-api/src/annotations.ts
index 3595c12220..904427a700 100644
--- a/packages/junior-plugin-api/src/annotations.ts
+++ b/packages/junior-plugin-api/src/annotations.ts
@@ -73,8 +73,6 @@ export interface ConversationSidebarHookContext extends PluginContext {
}
export interface ConversationSidebarResult {
- annotationsByConversationId: Record<
- string,
- ConversationSidebarAnnotation[]
- >;
+ /** Sidebar annotations in display order. Put the newest annotation first. */
+ annotationsByConversationId: Record;
}