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;