From 2630d4403fe78bb71682029a4ac170f80171c47c Mon Sep 17 00:00:00 2001 From: Mux Date: Thu, 13 Aug 2026 23:46:27 -0500 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=A4=96=20feat:=20notify=20workspaces?= =?UTF-8?q?=20about=20new=20GitHub=20PR=20reviews?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WorkspaceMenuBar/WorkspaceMenuBar.tsx | 83 +++ src/common/constants/experiments.ts | 9 + src/common/orpc/schemas/api.ts | 9 + src/common/orpc/schemas/workspace.ts | 4 + src/common/schemas/project.ts | 4 + src/common/types/message.ts | 6 + src/node/config.ts | 11 +- src/node/orpc/router.ts | 11 + .../githubReviewNotificationService.test.ts | 304 +++++++++ .../githubReviewNotificationService.ts | 611 ++++++++++++++++++ src/node/services/messageQueue.test.ts | 35 + src/node/services/serviceContainer.ts | 14 + src/node/services/workspaceService.ts | 35 + 13 files changed, 1135 insertions(+), 1 deletion(-) create mode 100644 src/node/services/githubReviewNotificationService.test.ts create mode 100644 src/node/services/githubReviewNotificationService.ts diff --git a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx index e047fad427b..b5ceacb48c9 100644 --- a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx +++ b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx @@ -74,6 +74,23 @@ 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; + onCheckedChange: (checked: boolean) => void; +}) { + return ( + + ); +} + export const WorkspaceMenuBar: React.FC = ({ workspaceId, projectName, @@ -91,6 +108,9 @@ 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); @@ -134,6 +154,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 +377,43 @@ export const WorkspaceMenuBar: React.FC = ({ } }, [api, getMoreMenuAnchor, runtimeStatusStore, stopRuntimeError, workspaceId]); + const handleGitHubReviewNotificationsChange = useCallback( + (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); + }); + }, + [api, getMoreMenuAnchor, githubReviewNotificationsError, workspaceId] + ); + const loadSkills = useCallback(async () => { const requestId = ++skillsRequestIdRef.current; @@ -601,6 +661,15 @@ export const WorkspaceMenuBar: React.FC = ({ + {githubReviewNotificationsExperimentEnabled && hasRepository && ( + { + handleGitHubReviewNotificationsChange(checked); + }} + /> + )} + {githubReviewNotificationsExperimentEnabled && hasRepository && ( + { + handleGitHubReviewNotificationsChange(checked); + }} + /> + )} ); } @@ -377,42 +383,39 @@ export const WorkspaceMenuBar: React.FC = ({ } }, [api, getMoreMenuAnchor, runtimeStatusStore, stopRuntimeError, workspaceId]); - const handleGitHubReviewNotificationsChange = useCallback( - (enabled: boolean): void => { - if (!api) { - githubReviewNotificationsError.showError( - workspaceId, - "Not connected to server", - getMoreMenuAnchor() - ); - return; - } + 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) => { + setGithubReviewNotificationsUpdatePending(true); + api.workspace.githubReviewNotifications + .set({ workspaceId, enabled }) + .then((result) => { + if (!result.success) { githubReviewNotificationsError.showError( workspaceId, - getErrorMessage(error), + result.error ?? "Failed to update GitHub review notifications", getMoreMenuAnchor() ); - }) - .finally(() => { - setGithubReviewNotificationsUpdatePending(false); - }); - }, - [api, getMoreMenuAnchor, githubReviewNotificationsError, workspaceId] - ); + } + }) + .catch((error: unknown) => { + githubReviewNotificationsError.showError( + workspaceId, + getErrorMessage(error), + getMoreMenuAnchor() + ); + }) + .finally(() => { + setGithubReviewNotificationsUpdatePending(false); + }); + }; const loadSkills = useCallback(async () => { const requestId = ++skillsRequestIdRef.current; @@ -441,6 +444,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 @@ -469,6 +476,29 @@ export const WorkspaceMenuBar: React.FC = ({ return () => window.removeEventListener("keydown", handler); }, [setNotifyOnResponse]); + useEffect(() => { + if (!githubReviewNotificationsExperimentEnabled || !hasRepository) { + 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, + workspaceEntry?.githubReviewNotificationsEnabled, + ]); + useEffect(() => { const handler = (e: KeyboardEvent) => { if (matchesKeybind(e, KEYBINDS.SHOW_WORKSPACE_DETAILS)) { @@ -665,9 +695,12 @@ export const WorkspaceMenuBar: React.FC = ({ { - handleGitHubReviewNotificationsChange(checked); - }} + shortcutLabel={ + isTouchMobileScreen + ? undefined + : formatKeybind(KEYBINDS.TOGGLE_GITHUB_REVIEW_NOTIFICATIONS) + } + onCheckedChange={handleGitHubReviewNotificationsChange} /> )}