From 73d73894d462ea075da4b615bc69e70db2f0be6a Mon Sep 17 00:00:00 2001 From: Rafael Audibert <32079912+rafaeelaudibert@users.noreply.github.com> Date: Thu, 28 May 2026 17:48:03 -0300 Subject: [PATCH 1/2] Add PR badge to sidebar task list Tasks in the sidebar with a linked pull request now show a small clickable badge on the right side of the row. The badge displays the PR number (e.g. `#123`) when it can be extracted from the URL and falls back to `PR` otherwise; clicking it opens the PR in the user's browser via `openUrlInBrowser`. The URL is sourced from the existing `TaskData.cloudPrUrl` field (populated from `task.latest_run.output.pr_url`), so no new data plumbing is needed. Generated-By: PostHog Code Task-Id: ba55cf1b-c69e-4b32-b689-9a00d3831b64 --- .../sidebar/components/TaskListView.tsx | 1 + .../sidebar/components/items/TaskItem.tsx | 50 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx index 9882e8bbf2..7a885f3b6b 100644 --- a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx +++ b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx @@ -136,6 +136,7 @@ function TaskRow({ slackThreadUrl={task.slackThreadUrl} prState={prState} hasDiff={hasDiff} + prUrl={task.cloudPrUrl} timestamp={timestamp} onClick={onClick} onDoubleClick={onDoubleClick} diff --git a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx index a5ee2a5b49..3c5b2c66db 100644 --- a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx +++ b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx @@ -1,13 +1,54 @@ import { Tooltip } from "@components/ui/Tooltip"; import type { SidebarPrState } from "@features/sidebar/hooks/useTaskPrStatus"; import type { WorkspaceMode } from "@main/services/workspace/schemas"; -import { Archive, PushPin } from "@phosphor-icons/react"; +import { Archive, GitPullRequest, PushPin } from "@phosphor-icons/react"; import type { TaskRunStatus } from "@shared/types"; +import { openUrlInBrowser } from "@utils/browser"; import { formatRelativeTimeShort } from "@utils/time"; import { useCallback, useEffect, useRef, useState } from "react"; import { SidebarItem } from "../SidebarItem"; import { TaskIcon } from "./TaskIcon"; +function extractPrNumber(url: string): string | null { + const match = url.match( + /\/(?:pull|pulls|merge_requests|pull-requests)\/(\d+)/, + ); + return match ? match[1] : null; +} + +function PrBadge({ url }: { url: string }) { + const number = extractPrNumber(url); + const open = () => { + void openUrlInBrowser(url); + }; + return ( + + {/* biome-ignore lint/a11y/useSemanticElements: nested clickable inside SidebarItem button */} + { + e.stopPropagation(); + open(); + }} + onDoubleClick={(e) => e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + open(); + } + }} + > + + {number ? `#${number}` : "PR"} + + + ); +} + interface TaskItemProps { depth?: number; taskId: string; @@ -27,6 +68,7 @@ interface TaskItemProps { slackThreadUrl?: string; prState?: SidebarPrState; hasDiff?: boolean; + prUrl?: string | null; timestamp?: number; isEditing?: boolean; onClick: (e: React.MouseEvent) => void; @@ -123,6 +165,7 @@ export function TaskItem({ slackThreadUrl, prState, hasDiff, + prUrl, timestamp, isEditing = false, onClick, @@ -149,6 +192,8 @@ export function TaskItem({ /> ); + const prBadge = prUrl ? : null; + const timestampNode = timestamp ? ( {formatRelativeTimeShort(timestamp)} @@ -165,8 +210,9 @@ export function TaskItem({ ) : null; const endContent = - timestampNode || toolbar ? ( + prBadge || timestampNode || toolbar ? ( <> + {prBadge} {timestampNode} {toolbar} From 74e293d87921fe0ca6851d391ce8b9393dac62e0 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Thu, 4 Jun 2026 17:01:28 -0700 Subject: [PATCH 2/2] Extract NestedButton and reuse shared PR parser --- .../components/ui/NestedButton.test.tsx | 34 ++++++ .../renderer/components/ui/NestedButton.tsx | 50 +++++++++ .../sidebar/components/items/TaskIcon.tsx | 38 ++----- .../sidebar/components/items/TaskItem.tsx | 103 ++++++------------ 4 files changed, 125 insertions(+), 100 deletions(-) create mode 100644 apps/code/src/renderer/components/ui/NestedButton.test.tsx create mode 100644 apps/code/src/renderer/components/ui/NestedButton.tsx diff --git a/apps/code/src/renderer/components/ui/NestedButton.test.tsx b/apps/code/src/renderer/components/ui/NestedButton.test.tsx new file mode 100644 index 0000000000..d5cf6bc20c --- /dev/null +++ b/apps/code/src/renderer/components/ui/NestedButton.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { NestedButton } from "./NestedButton"; + +describe("NestedButton", () => { + it("renders an accessible button", () => { + render( {}}>x); + expect(screen.getByRole("button")).toBeTruthy(); + }); + + it("calls onActivate on click without bubbling to the parent", async () => { + const onActivate = vi.fn(); + const onParentClick = vi.fn(); + render( + // biome-ignore lint/a11y/useKeyWithClickEvents: test-only wrapper + // biome-ignore lint/a11y/noStaticElementInteractions: test-only wrapper +
+ x +
, + ); + await userEvent.click(screen.getByRole("button")); + expect(onActivate).toHaveBeenCalledTimes(1); + expect(onParentClick).not.toHaveBeenCalled(); + }); + + it.each(["{Enter}", " "])("activates with the %s key", async (key) => { + const onActivate = vi.fn(); + render(x); + screen.getByRole("button").focus(); + await userEvent.keyboard(key); + expect(onActivate).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/code/src/renderer/components/ui/NestedButton.tsx b/apps/code/src/renderer/components/ui/NestedButton.tsx new file mode 100644 index 0000000000..8c43854840 --- /dev/null +++ b/apps/code/src/renderer/components/ui/NestedButton.tsx @@ -0,0 +1,50 @@ +import type React from "react"; +import { forwardRef } from "react"; + +interface NestedButtonProps extends React.HTMLAttributes { + onActivate: () => void; +} + +/** + * A button nested inside another button. Rows like SidebarItem render as a real + * `