Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const clientSettings: ClientSettings = {
fontSizeTerminal: 12,
fontSmoothing: true,
glassOpacity: 80,
openPullRequestLinksInApp: false,
planModeEnabled: false,
showSkillsInSlashMenu: false,
providerModelPreferences: {},
Expand Down
32 changes: 32 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
? ["Diff whitespace changes"]
: []),
...(settings.openPullRequestLinksInApp !== DEFAULT_UNIFIED_SETTINGS.openPullRequestLinksInApp
? ["Open pull request links in T3 Code"]
: []),
...(settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu
? ["Show skills in slash menu"]
: []),
Expand Down Expand Up @@ -572,6 +575,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.glassOpacity,
settings.enableLegacyTokenStreaming,
settings.enableProviderUpdateChecks,
settings.openPullRequestLinksInApp,
settings.sidebarAutoSettleAfterDays,
settings.sidebarAutoSettleOnMerge,
settings.sidebarProjectGroupingMode,
Expand Down Expand Up @@ -652,6 +656,7 @@ export function useSettingsRestore(onRestored?: () => void) {
timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
openPullRequestLinksInApp: DEFAULT_UNIFIED_SETTINGS.openPullRequestLinksInApp,
showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,
environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode,
glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
Expand Down Expand Up @@ -2081,6 +2086,33 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
{...searchableSetting("pull-request-links-in-app")}
description="Open pull request links from a thread in the review panel. Turn this off to send every click to your browser; command-click (control-click on Windows and Linux) always opens the browser."
resetAction={
settings.openPullRequestLinksInApp !==
DEFAULT_UNIFIED_SETTINGS.openPullRequestLinksInApp ? (
<SettingResetButton
label="pull request links"
onClick={() =>
updateSettings({
openPullRequestLinksInApp: DEFAULT_UNIFIED_SETTINGS.openPullRequestLinksInApp,
})
}
/>
) : null
}
control={
<Switch
checked={settings.openPullRequestLinksInApp}
onCheckedChange={(checked) =>
updateSettings({ openPullRequestLinksInApp: Boolean(checked) })
}
aria-label="Open pull request links in T3 Code"
/>
}
/>

<SettingsRow
{...searchableSetting("skills-in-slash-menu")}
description="Also include skills in the / command menu. Skills always appear when you type $."
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ export const SETTINGS_SEARCH_ITEMS = [
title: "Hide whitespace changes",
to: "/settings/general",
},
{
id: "pull-request-links-in-app",
title: "Open pull request links in T3 Code",
to: "/settings/general",
},
{
id: "skills-in-slash-menu",
title: "Show skills in slash menu",
Expand Down
11 changes: 8 additions & 3 deletions apps/web/src/lib/openPullRequestLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,17 @@ describe("openPullRequestLink", () => {

describe("shouldOpenPullRequestExternally", () => {
it("uses the browser for command-click and control-click", () => {
expect(shouldOpenPullRequestExternally({ metaKey: true, ctrlKey: false })).toBe(true);
expect(shouldOpenPullRequestExternally({ metaKey: false, ctrlKey: true })).toBe(true);
expect(shouldOpenPullRequestExternally({ metaKey: true, ctrlKey: false }, true)).toBe(true);
expect(shouldOpenPullRequestExternally({ metaKey: false, ctrlKey: true }, true)).toBe(true);
});

it("keeps an unmodified click in the pull request view", () => {
expect(shouldOpenPullRequestExternally({ metaKey: false, ctrlKey: false })).toBe(false);
expect(shouldOpenPullRequestExternally({ metaKey: false, ctrlKey: false }, true)).toBe(false);
});

it("sends every click to the browser once the in-app preference is off", () => {
expect(shouldOpenPullRequestExternally({ metaKey: false, ctrlKey: false }, false)).toBe(true);
expect(shouldOpenPullRequestExternally({ metaKey: true, ctrlKey: false }, false)).toBe(true);
});
});

Expand Down
18 changes: 13 additions & 5 deletions apps/web/src/lib/openPullRequestLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { type MouseEvent, useCallback } from "react";
import { pullRequestHostOf, type SourceControlProviderKind } from "@t3tools/contracts";

import { stackedThreadToast, toastManager } from "../components/ui/toast";
import { useClientSettings } from "../hooks/useSettings";
import { readLocalApi } from "../localApi";
import { useRightPanelStore } from "../rightPanelStore";
import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
Expand Down Expand Up @@ -210,11 +211,16 @@ export function findProjectForChangeRequest(
* the pull requests page: a reader following a link the agent wrote is reading the thread, and
* should still be reading it afterwards. Any change request opens there, not only the thread's
* own, since the panel is told which one to show.
*
* Settings → General turns this off for readers who want every pull request link in their
* browser, in which case nothing here claims a link at all.
*/
/** Command-click (control-click off macOS) always means the browser; `openInApp` decides the rest. */
export function shouldOpenPullRequestExternally(
event: Pick<MouseEvent<HTMLElement>, "metaKey" | "ctrlKey">,
openInApp: boolean,
): boolean {
return event.metaKey || event.ctrlKey;
return !openInApp || event.metaKey || event.ctrlKey;
}

export function useOpenChangeRequestLink(
Expand All @@ -228,12 +234,13 @@ export function useOpenChangeRequestLink(
targetThreadRef?: ScopedThreadRef,
) => boolean {
const navigate = useNavigate();
const openInApp = useClientSettings((settings) => settings.openPullRequestLinksInApp);
const allProjects = useProjects();
const serverConfigs = useServerConfigs();
const primaryEnvironmentId = usePrimaryEnvironmentId();
return useCallback(
(event, targetUrl, targetThreadRef) => {
if (shouldOpenPullRequestExternally(event)) return false;
if (shouldOpenPullRequestExternally(event, openInApp)) return false;
const resolvedThreadRef = targetThreadRef ?? threadRef;
const parsed = parseChangeRequestUrl(targetUrl);
if (parsed === null) return false;
Expand Down Expand Up @@ -285,16 +292,17 @@ export function useOpenChangeRequestLink(
});
return true;
},
[allProjects, navigate, primaryEnvironmentId, serverConfigs, threadRef],
[allProjects, navigate, openInApp, primaryEnvironmentId, serverConfigs, threadRef],
);
}

export function useOpenPrLink(threadRef?: ScopedThreadRef) {
const openChangeRequest = useOpenChangeRequestLink(threadRef);
const openInApp = useClientSettings((settings) => settings.openPullRequestLinksInApp);
return useCallback(
(event: MouseEvent<HTMLElement>, prUrl: string, targetThreadRef?: ScopedThreadRef) => {
event.stopPropagation();
const openInBrowser = shouldOpenPullRequestExternally(event);
const openInBrowser = shouldOpenPullRequestExternally(event, openInApp);
const isAnchor =
event.currentTarget instanceof HTMLAnchorElement && event.currentTarget.href.length > 0;
// A real link already knows how to cmd/ctrl+click. Leave its default
Expand Down Expand Up @@ -326,6 +334,6 @@ export function useOpenPrLink(threadRef?: ScopedThreadRef) {
});
return false;
},
[openChangeRequest],
[openChangeRequest, openInApp],
);
}
1 change: 1 addition & 0 deletions docs/user/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ T3 Code works with the platforms your team already uses:
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
- Prefer your browser for everything? Turn off **Settings → General → Open pull request links in T3 Code**, and every pull request link opens there instead
- Check out a teammate's branch to review code locally

**Fix what you wrote, in place**
Expand Down
5 changes: 5 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ export const ClientSettingsSchema = Schema.Struct({
modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
}),
).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
// Where an unmodified click on a pull request link lands: the right-panel
// review beside the conversation, or the browser. Off makes every click
// behave the way command-click already does.
openPullRequestLinksInApp: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
// Legacy plan mode. The composer's Build/Plan toggle was removed from the
// default UI; this beta flag restores it (plus the /plan and /default slash
// commands) for users who still rely on the old workflow.
Expand Down Expand Up @@ -938,6 +942,7 @@ export const ClientSettingsPatch = Schema.Struct({
}),
),
),
openPullRequestLinksInApp: Schema.optionalKey(Schema.Boolean),
planModeEnabled: Schema.optionalKey(Schema.Boolean),
showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),
legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),
Expand Down
Loading