diff --git a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx
index e047fad427b..da8956e8ba3 100644
--- a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx
+++ b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx
@@ -20,7 +20,11 @@ import { formatKeybind, KEYBINDS, matchesKeybind } from "@/browser/utils/ui/keyb
import { useRuntimeStatus, useRuntimeStatusStoreRaw } from "@/browser/stores/RuntimeStatusStore";
import { useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore";
import { Button } from "@/browser/components/Button/Button";
-import { isDevcontainerRuntime, type RuntimeConfig } from "@/common/types/runtime";
+import {
+ isDevcontainerRuntime,
+ supportsGitHubReviewNotifications,
+ type RuntimeConfig,
+} from "@/common/types/runtime";
import { useTutorial } from "@/browser/contexts/TutorialContext";
import type { TerminalSessionCreateOptions } from "@/browser/utils/terminal";
@@ -47,6 +51,7 @@ import { formatProjectHierarchyLabel } from "@/common/utils/subProjects";
import { forkWorkspace } from "@/browser/utils/chatCommands";
import { SCRATCH_PROJECT_CONFIG_KEY, SCRATCH_PROJECT_NAME } from "@/common/constants/scratch";
import { hasWorkspaceRepository } from "@/browser/utils/workspaceCapabilities";
+import { isMultiProject } from "@/common/utils/multiProject";
import { stopKeyboardPropagation } from "@/browser/utils/events";
import { WORKSPACE_MENU_BAR_LEFT_SIDEBAR_COLLAPSED_PADDING_PX } from "@/constants/layout";
import type { AgentSkillDescriptor, AgentSkillIssue } from "@/common/types/agentSkill";
@@ -74,6 +79,29 @@ const COLLAPSED_LEFT_SIDEBAR_MENU_BAR_STYLE = {
paddingLeft: `${WORKSPACE_MENU_BAR_LEFT_SIDEBAR_COLLAPSED_PADDING_PX}px`,
} as const;
+function GitHubReviewNotificationsOption(props: {
+ checked: boolean;
+ disabled: boolean;
+ shortcutLabel?: string;
+ onCheckedChange: (checked: boolean) => void;
+}) {
+ return (
+
+ );
+}
+
export const WorkspaceMenuBar: React.FC = ({
workspaceId,
projectName,
@@ -91,11 +119,19 @@ export const WorkspaceMenuBar: React.FC = ({
const { preflightArchiveWorkspace, archiveWorkspace, setWorkspacePinned } = useWorkspaceActions();
const { workspaceMetadata } = useWorkspaceContext();
const workspaceHeartbeatsEnabled = useExperimentValue(EXPERIMENT_IDS.WORKSPACE_HEARTBEATS);
+ const githubReviewNotificationsExperimentEnabled = useExperimentValue(
+ EXPERIMENT_IDS.GITHUB_PR_REVIEW_NOTIFICATIONS
+ );
const openTerminalPopout = useOpenTerminal();
const openInEditor = useOpenInEditor();
const runtimeStatus = useRuntimeStatus(workspaceId);
const workspaceEntry = workspaceMetadata.get(workspaceId);
const hasRepository = hasWorkspaceRepository(workspaceEntry);
+ // Do not offer a setting that cannot poll without starting remote infrastructure.
+ const githubReviewNotificationsSupported = supportsGitHubReviewNotifications(
+ runtimeConfig,
+ workspaceEntry != null && isMultiProject(workspaceEntry)
+ );
// The workspace's metadata.projectName is the parent project (since worktrees
// are owned by the top-most parent). When the workspace is scoped to a
// sub-project we surface the hierarchy as "parent / child" so the menu bar
@@ -134,6 +170,9 @@ export const WorkspaceMenuBar: React.FC = ({
const archiveError = usePopoverError();
const forkError = usePopoverError();
const stopRuntimeError = usePopoverError();
+ const githubReviewNotificationsError = usePopoverError();
+ const [githubReviewNotificationsUpdatePending, setGithubReviewNotificationsUpdatePending] =
+ useState(false);
const [rightSidebarCollapsed] = usePersistedState(RIGHT_SIDEBAR_COLLAPSED_KEY, false, {
// This state is toggled from RightSidebar, so we need cross-component updates.
@@ -354,6 +393,40 @@ export const WorkspaceMenuBar: React.FC = ({
}
}, [api, getMoreMenuAnchor, runtimeStatusStore, stopRuntimeError, workspaceId]);
+ const handleGitHubReviewNotificationsChange = (enabled: boolean): void => {
+ if (!api) {
+ githubReviewNotificationsError.showError(
+ workspaceId,
+ "Not connected to server",
+ getMoreMenuAnchor()
+ );
+ return;
+ }
+
+ setGithubReviewNotificationsUpdatePending(true);
+ api.workspace.githubReviewNotifications
+ .set({ workspaceId, enabled })
+ .then((result) => {
+ if (!result.success) {
+ githubReviewNotificationsError.showError(
+ workspaceId,
+ result.error ?? "Failed to update GitHub review notifications",
+ getMoreMenuAnchor()
+ );
+ }
+ })
+ .catch((error: unknown) => {
+ githubReviewNotificationsError.showError(
+ workspaceId,
+ getErrorMessage(error),
+ getMoreMenuAnchor()
+ );
+ })
+ .finally(() => {
+ setGithubReviewNotificationsUpdatePending(false);
+ });
+ };
+
const loadSkills = useCallback(async () => {
const requestId = ++skillsRequestIdRef.current;
@@ -381,6 +454,10 @@ export const WorkspaceMenuBar: React.FC = ({
}
}, [api, workspaceId, disableWorkspaceAgents]);
+ const githubReviewNotificationsChangeRef = useRef<(enabled: boolean) => void>(() => undefined);
+ // Keep the global shortcut listener stable while it reads the latest API and workspace state.
+ githubReviewNotificationsChangeRef.current = handleGitHubReviewNotificationsChange;
+
// Start workspace tutorial on first entry
useEffect(() => {
// Small delay to ensure UI is rendered
@@ -409,6 +486,34 @@ export const WorkspaceMenuBar: React.FC = ({
return () => window.removeEventListener("keydown", handler);
}, [setNotifyOnResponse]);
+ useEffect(() => {
+ if (
+ !githubReviewNotificationsExperimentEnabled ||
+ !hasRepository ||
+ !githubReviewNotificationsSupported
+ ) {
+ return;
+ }
+
+ const handler = (e: KeyboardEvent) => {
+ if (!matchesKeybind(e, KEYBINDS.TOGGLE_GITHUB_REVIEW_NOTIFICATIONS)) {
+ return;
+ }
+
+ e.preventDefault();
+ githubReviewNotificationsChangeRef.current(
+ workspaceEntry?.githubReviewNotificationsEnabled !== true
+ );
+ };
+ window.addEventListener("keydown", handler);
+ return () => window.removeEventListener("keydown", handler);
+ }, [
+ githubReviewNotificationsExperimentEnabled,
+ hasRepository,
+ githubReviewNotificationsSupported,
+ workspaceEntry?.githubReviewNotificationsEnabled,
+ ]);
+
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (matchesKeybind(e, KEYBINDS.SHOW_WORKSPACE_DETAILS)) {
@@ -601,6 +706,20 @@ export const WorkspaceMenuBar: React.FC = ({
+ {githubReviewNotificationsExperimentEnabled &&
+ hasRepository &&
+ githubReviewNotificationsSupported && (
+
+ )}
+ {githubReviewNotificationsExperimentEnabled &&
+ hasRepository &&
+ githubReviewNotificationsSupported && (
+
+ )}