Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions apps/web/src/components/LegacySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ import {
getSidebarThreadIdsToPrewarm,
resolveAdjacentThreadId,
isContextMenuPointerDown,
isSidebarNestedLinkClick,
isTrailingDoubleClick,
resolveProjectStatusIndicator,
resolveThreadRowClassName,
Expand Down Expand Up @@ -567,7 +568,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
[clearSelection, handleMultiSelectContextMenu, handleThreadContextMenu, isSelected, threadRef],
);
const handlePrClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (!prStatus) return;
const openedInRightPanel = openPrLink(
event,
Expand Down Expand Up @@ -694,14 +695,17 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
<a
href={prStatus.url}
target="_blank"
rel="noopener noreferrer"
aria-label={prStatus.tooltip}
className={`inline-flex items-center justify-center ${prStatus.colorClass} cursor-pointer rounded-sm outline-hidden focus-visible:ring-1 focus-visible:ring-ring`}
onPointerDown={(event) => event.stopPropagation()}
onClick={handlePrClick}
>
<ChangeRequestStatusIcon className="size-3" />
</button>
</a>
}
/>
<TooltipPopup side="top">
Expand Down Expand Up @@ -1727,6 +1731,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
threadRef: ScopedThreadRef,
orderedProjectThreadKeys: readonly string[],
) => {
if (isSidebarNestedLinkClick(event.target)) return;
const isMac = isMacPlatform(navigator.platform);
const isModClick = isMac ? event.metaKey : event.ctrlKey;
const isShiftClick = event.shiftKey;
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getProjectSortTimestamp,
hasUnseenCompletion,
isContextMenuPointerDown,
isSidebarNestedLinkClick,
isTrailingDoubleClick,
orderItemsByPreferredIds,
resolveProjectStatusIndicator,
Expand Down Expand Up @@ -411,6 +412,27 @@ describe("isTrailingDoubleClick", () => {
});
});

describe("isSidebarNestedLinkClick", () => {
const linkTarget = {
closest: (selector: string) => (selector === "a[href]" ? ({} as Element) : null),
} as unknown as EventTarget;

it("ignores row clicks that originated on a nested link", () => {
expect(isSidebarNestedLinkClick(linkTarget)).toBe(true);
});

it("walks up from a text node to the enclosing link", () => {
expect(isSidebarNestedLinkClick({ parentElement: linkTarget } as unknown as EventTarget)).toBe(
true,
);
});

it("leaves ordinary row clicks alone", () => {
expect(isSidebarNestedLinkClick({ closest: () => null } as unknown as EventTarget)).toBe(false);
expect(isSidebarNestedLinkClick(null)).toBe(false);
});
});

describe("shouldCreateNewThreadInCurrentProject", () => {
it("creates directly on shift+click in a multi-project setup", () => {
expect(shouldCreateNewThreadInCurrentProject(true, 2)).toBe(true);
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,24 @@ export function isTrailingDoubleClick(detail: number): boolean {
return detail > 1;
}

function nodeClosest(node: object | null, selector: string): unknown {
if (node === null || !("closest" in node) || typeof node.closest !== "function") return null;
return node.closest(selector);
}

/** Clicks on a nested link keep the link's meaning. The row must not treat them as multi-select. */
export function isSidebarNestedLinkClick(target: EventTarget | null): boolean {
if (target == null || typeof target !== "object") return false;
if (nodeClosest(target, "a[href]") !== null) return true;
const parent =
"parentElement" in target &&
target.parentElement !== null &&
typeof target.parentElement === "object"
? target.parentElement
: null;
return nodeClosest(parent, "a[href]") !== null;
}

// Shift+click on the new thread button creates directly in the current
// project, skipping the command palette's project picker. With a single
// project there is nothing to pick, so a plain click already creates
Expand Down
15 changes: 11 additions & 4 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import {
formatWorkingDurationLabel,
firstValidTimestampMs,
hasUnseenCompletion,
isSidebarNestedLinkClick,
isTrailingDoubleClick,
orderItemsByPreferredIds,
planPinnedReorder,
Expand Down Expand Up @@ -1006,7 +1007,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
if (!showSnoozeButton) setSnoozeMenuOpen(false);
}, [showSnoozeButton]);
const handlePrClick = useCallback(
(event: ReactMouseEvent<HTMLElement>) => {
(event: ReactMouseEvent<HTMLAnchorElement>) => {
if (!pr?.url) return;
const openedInRightPanel = openPrLink(
event,
Expand Down Expand Up @@ -1083,10 +1084,15 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
</span>
);

// A real link so cmd/ctrl+click and middle-click open the host in the
// browser. A plain click still opens T3's pull request view.
const prBadge =
prStatus && pr ? (
<button
type="button"
<a
href={pr.url}
target="_blank"
rel="noopener noreferrer"
onPointerDown={(event) => event.stopPropagation()}
onClick={handlePrClick}
className={cn(
// Sidebar chrome follows the interface font; tabular digits keep the
Expand All @@ -1101,7 +1107,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
aria-label={prStatus.tooltip}
>
#{pr.number}
</button>
</a>
) : null;
const terminalStatusIcon = terminalStatus ? (
<span
Expand Down Expand Up @@ -2307,6 +2313,7 @@ export default function Sidebar() {

const handleThreadClick = useCallback(
(event: ReactMouseEvent, threadRef: ScopedThreadRef) => {
if (isSidebarNestedLinkClick(event.target)) return;
const isMac = isMacPlatform(navigator.platform);
const isModClick = isMac ? event.metaKey : event.ctrlKey;
const threadKey = scopedThreadKey(threadRef);
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/lib/openPullRequestLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,17 @@ export function useOpenPrLink(threadRef?: ScopedThreadRef) {
const openChangeRequest = useOpenChangeRequestLink(threadRef);
return useCallback(
(event: MouseEvent<HTMLElement>, prUrl: string, targetThreadRef?: ScopedThreadRef) => {
event.preventDefault();
event.stopPropagation();
if (openChangeRequest(event, prUrl, targetThreadRef)) return true;
const openInBrowser = shouldOpenPullRequestExternally(event);
const isAnchor =
event.currentTarget instanceof HTMLAnchorElement && event.currentTarget.href.length > 0;
// A real link already knows how to cmd/ctrl+click. Leave its default
// action alone so the browser (or Electron's window-open handler) opens
// the host. Buttons have no href, so they still go through openExternal.
if (openInBrowser && isAnchor) return false;

event.preventDefault();
if (!openInBrowser && openChangeRequest(event, prUrl, targetThreadRef)) return true;

const api = readLocalApi();
if (!api) {
Expand Down
1 change: 1 addition & 0 deletions docs/user/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ T3 Code works with the platforms your team already uses:
- While working in a thread, open linked reviews in the same compact right-panel tabs without
leaving the conversation
- Open the review directly in your browser with one click
- Command-click (Control-click on Windows and Linux) a pull request number in the sidebar to open it in your browser instead of in T3 Code
- Check out a teammate's branch to review code locally

**Fix what you wrote, in place**
Expand Down
Loading