diff --git a/apps/web/src/components/chat/AgentsPanel.browser.tsx b/apps/web/src/components/chat/AgentsPanel.browser.tsx index 344669c13..373f029e6 100644 --- a/apps/web/src/components/chat/AgentsPanel.browser.tsx +++ b/apps/web/src/components/chat/AgentsPanel.browser.tsx @@ -1540,6 +1540,41 @@ describe("AgentsPanel", () => { } }); + it("gives an inactive tab distinct resting and hover surfaces", async () => { + const mounted = await render( +
+ +
+ +
, + ); + + try { + await expect.element(page.getByRole("tab", { name: "Agents" })).toBeVisible(); + const panel = document.querySelector("[data-chat-right-panel='true']") as HTMLElement; + const inactiveTab = document.querySelector("[data-right-panel-tab='agents']") as HTMLElement; + const panelBackground = getComputedStyle(panel).backgroundColor; + const restingBackground = getComputedStyle(inactiveTab).backgroundColor; + + expect(restingBackground).not.toBe(panelBackground); + + await page.getByRole("tab", { name: "Agents" }).hover(); + await vi.waitFor(() => { + if (getComputedStyle(inactiveTab).backgroundColor === restingBackground) { + throw new Error("The inactive tab never gained its separate hover surface."); + } + }); + } finally { + await mounted.unmount(); + } + }); + it("parks the + at the strip's left edge while the launcher is showing", async () => { const mounted = await render(
diff --git a/apps/web/src/components/chat/RightPanelTabStrip.tsx b/apps/web/src/components/chat/RightPanelTabStrip.tsx index e0652ed1d..ec4d07e52 100644 --- a/apps/web/src/components/chat/RightPanelTabStrip.tsx +++ b/apps/web/src/components/chat/RightPanelTabStrip.tsx @@ -157,7 +157,7 @@ function TabStripItem({ "pr-3.5 pl-1.5", active ? "bg-background text-foreground" - : "text-muted-foreground/80 hover:bg-accent hover:text-foreground", + : "bg-muted/50 text-muted-foreground/80 hover:bg-accent hover:text-foreground", )} {...(measuring ? {} diff --git a/scripts/generate-release-notes.test.ts b/scripts/generate-release-notes.test.ts index c36658df7..5d559567d 100644 --- a/scripts/generate-release-notes.test.ts +++ b/scripts/generate-release-notes.test.ts @@ -119,14 +119,14 @@ it("uses GitHub PR attribution, filters release-preparation noise, and keeps dir [ "## What's Changed", "", - "- Add prompt stashing by @will in https://github.com/Threadlines/threadlines/pull/93", + "- Add prompt stashing by [@will](https://github.com/will) in https://github.com/Threadlines/threadlines/pull/93", "", "### Direct changes", "", "- [`cccccccc`](https://github.com/Threadlines/threadlines/commit/cccccccccccccccccccccccccccccccccccccccc) Keep updater diagnostics visible", "", "## New Contributors", - "* @will made their first contribution in https://github.com/Threadlines/threadlines/pull/93", + "* [@will](https://github.com/will) made their first contribution in https://github.com/Threadlines/threadlines/pull/93", "", "**Full Changelog**: https://github.com/Threadlines/threadlines/compare/v0.3.1-nightly.20260731.204...v0.3.1-nightly.20260731.205", "", diff --git a/scripts/generate-release-notes.ts b/scripts/generate-release-notes.ts index d525a864d..2f87b573a 100644 --- a/scripts/generate-release-notes.ts +++ b/scripts/generate-release-notes.ts @@ -249,6 +249,26 @@ interface ParsedGitHubGeneratedNotes { readonly fullChangelogLine: string | undefined; } +function githubProfileLink(login: string): string { + return `[@${login}](https://github.com/${login})`; +} + +/** GitHub gives bare @mentions a viewer-specific highlight. Keep attribution + * clickable without that highlight by rendering the generated author patterns + * as ordinary profile links. Limit this to GitHub's own templates so an @ in a + * pull request title or email-like text is not rewritten. */ +function linkGitHubGeneratedAttribution(line: string): string { + return line + .replace( + /\bby @([a-z\d](?:[a-z\d-]{0,37}[a-z\d])?)(?=\s+in\s+)/gi, + (_match, login: string) => `by ${githubProfileLink(login)}`, + ) + .replace( + /^([-*]\s+)@([a-z\d](?:[a-z\d-]{0,37}[a-z\d])?)(?=\s+made their first contribution\b)/i, + (_match, bullet: string, login: string) => `${bullet}${githubProfileLink(login)}`, + ); +} + function parseGitHubGeneratedNotes(body: string): ParsedGitHubGeneratedNotes { const lines = body.replaceAll("\r\n", "\n").split("\n"); const newContributorsStart = lines.findIndex((line) => /^##\s+New Contributors\s*$/i.test(line)); @@ -272,7 +292,7 @@ function parseGitHubGeneratedNotes(body: string): ParsedGitHubGeneratedNotes { if (pullRequestNumber) filteredPullRequestNumbers.add(pullRequestNumber); return false; }) - .map((line) => line.trim().replace(/^\*\s+/, "- ")); + .map((line) => linkGitHubGeneratedAttribution(line.trim().replace(/^\*\s+/, "- "))); const newContributorSection = newContributorsStart === -1 @@ -286,7 +306,7 @@ function parseGitHubGeneratedNotes(body: string): ParsedGitHubGeneratedNotes { const pullRequestNumber = /\/pull\/(\d+)/i.exec(line)?.[1]; return !pullRequestNumber || !filteredPullRequestNumbers.has(pullRequestNumber); }) - .map((line) => line.trimEnd()) + .map((line) => linkGitHubGeneratedAttribution(line.trimEnd())) .filter((line, index, section) => { if (line.length > 0) return true; return index > 0 && index < section.length - 1;