Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
83 changes: 83 additions & 0 deletions src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<label className="flex cursor-pointer items-start gap-2">
<Checkbox
checked={props.checked}
disabled={props.disabled}
onCheckedChange={(checked) => props.onCheckedChange(checked === true)}
/>
<span className="text-muted-foreground">Notify when a GitHub PR review is posted</span>
</label>
);
}

export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
workspaceId,
projectName,
Expand All @@ -91,6 +108,9 @@ export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
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);
Expand Down Expand Up @@ -134,6 +154,9 @@ export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
const archiveError = usePopoverError();
const forkError = usePopoverError();
const stopRuntimeError = usePopoverError();
const githubReviewNotificationsError = usePopoverError();
const [githubReviewNotificationsUpdatePending, setGithubReviewNotificationsUpdatePending] =
useState(false);

const [rightSidebarCollapsed] = usePersistedState<boolean>(RIGHT_SIDEBAR_COLLAPSED_KEY, false, {
// This state is toggled from RightSidebar, so we need cross-component updates.
Expand Down Expand Up @@ -354,6 +377,43 @@ export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
}
}, [api, getMoreMenuAnchor, runtimeStatusStore, stopRuntimeError, workspaceId]);

const handleGitHubReviewNotificationsChange = useCallback(
(enabled: boolean): void => {
Comment thread
coadler marked this conversation as resolved.
Outdated
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;

Expand Down Expand Up @@ -601,6 +661,15 @@ export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
</span>
</span>
</label>
{githubReviewNotificationsExperimentEnabled && hasRepository && (
<GitHubReviewNotificationsOption
checked={workspaceEntry?.githubReviewNotificationsEnabled === true}
disabled={githubReviewNotificationsUpdatePending}
onCheckedChange={(checked) => {
handleGitHubReviewNotificationsChange(checked);
}}
/>
Comment thread
coadler marked this conversation as resolved.
Outdated
)}
<label className="flex cursor-pointer items-start gap-2">
<Checkbox
checked={autoEnableNotifications}
Expand Down Expand Up @@ -643,6 +712,15 @@ export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
</span>
</span>
</label>
{githubReviewNotificationsExperimentEnabled && hasRepository && (
<GitHubReviewNotificationsOption
checked={workspaceEntry?.githubReviewNotificationsEnabled === true}
disabled={githubReviewNotificationsUpdatePending}
onCheckedChange={(checked) => {
handleGitHubReviewNotificationsChange(checked);
}}
/>
)}
<label className="flex cursor-pointer items-start gap-2">
<Checkbox
checked={autoEnableNotifications}
Expand Down Expand Up @@ -842,6 +920,11 @@ export const WorkspaceMenuBar: React.FC<WorkspaceMenuBarProps> = ({
prefix="Failed to fork chat"
onDismiss={forkError.clearError}
/>
<PopoverError
error={githubReviewNotificationsError.error}
prefix="Failed to update GitHub review notifications"
onDismiss={githubReviewNotificationsError.clearError}
/>
<PopoverError
error={archiveError.error}
prefix="Failed to archive chat"
Expand Down
9 changes: 9 additions & 0 deletions src/common/constants/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const EXPERIMENT_IDS = {
AGENT_PLUGINS: "agent-plugins",
SKILL_DYNAMIC_CONTEXT: "skill-dynamic-context",
TIMELINE: "timeline",
GITHUB_PR_REVIEW_NOTIFICATIONS: "github-pr-review-notifications",
} as const;

export type ExperimentId = (typeof EXPERIMENT_IDS)[keyof typeof EXPERIMENT_IDS];
Expand Down Expand Up @@ -194,6 +195,14 @@ export const EXPERIMENTS: Record<ExperimentId, ExperimentDefinition> = {
enabledByDefault: false,
showInSettings: true,
},
[EXPERIMENT_IDS.GITHUB_PR_REVIEW_NOTIFICATIONS]: {
id: EXPERIMENT_IDS.GITHUB_PR_REVIEW_NOTIFICATIONS,
name: "GitHub PR review notifications",
description:
"Notify enabled workspaces when a new review is posted on their GitHub pull request",
enabledByDefault: false,
showInSettings: true,
},
};

function getPlatformDisplayName(platform: NodeJS.Platform): string {
Expand Down
9 changes: 9 additions & 0 deletions src/common/orpc/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,15 @@ export const workspace = {
output: ResultSchema(z.void(), z.string()),
},
},
githubReviewNotifications: {
set: {
input: z.object({
workspaceId: z.string(),
enabled: z.boolean(),
}),
output: ResultSchema(z.void(), z.string()),
},
},
goalDefaults: {
// Per-workspace override of the global `goalDefaults` block. `get`
// returns `null` when no override is set (i.e., this workspace uses
Expand Down
4 changes: 4 additions & 0 deletions src/common/orpc/schemas/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ export const WorkspaceMetadataSchema = z.object({
heartbeat: WorkspaceHeartbeatSettingsSchema.optional().meta({
description: "Persisted heartbeat settings for this workspace.",
}),
githubReviewNotificationsEnabled: z.boolean().optional().meta({
description:
"Whether this workspace receives notifications for new GitHub pull request reviews.",
}),
goalDefaults: WorkspaceGoalDefaultsOverrideSchema.optional().meta({
description:
"Per-workspace overrides for goal creation defaults (budget, turn cap, explicit-budget). Layered on top of the global `goalDefaults` from app config.",
Expand Down
4 changes: 4 additions & 0 deletions src/common/schemas/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export const WorkspaceConfigSchema = z.object({
heartbeat: WorkspaceHeartbeatSettingsSchema.optional().meta({
description: "Persisted heartbeat settings for this workspace.",
}),
githubReviewNotificationsEnabled: z.boolean().optional().meta({
description:
"Whether this workspace receives notifications for new GitHub pull request reviews.",
}),
goalDefaults: WorkspaceGoalDefaultsOverrideSchema.optional().meta({
description:
"Per-workspace overrides for goal creation defaults. Sparse; each null field follows the global `goalDefaults`.",
Expand Down
6 changes: 6 additions & 0 deletions src/common/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,12 @@ export type MuxMessageMetadata = MuxMessageMetadataBase &
*/
firedAt?: number;
}
| {
/** Synthetic provider-visible message for reviews posted on the linked GitHub PR. */
type: "github-pr-review-notification";
prUrl: string;
reviewIds: string[];
}
| {
type: "normal"; // Regular messages
/** Original user input for one-shot overrides (e.g., "/opus+high do something") — used as display content so the command prefix remains visible. */
Expand Down
11 changes: 10 additions & 1 deletion src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,7 @@ export class Config {
runtimeConfig: workspace.runtimeConfig ?? DEFAULT_RUNTIME_CONFIG,
aiSettings: workspace.aiSettings,
heartbeat: normalizeWorkspaceMetadataHeartbeat(workspace.heartbeat, config),
githubReviewNotificationsEnabled: workspace.githubReviewNotificationsEnabled,
goalDefaults: workspace.goalDefaults,
aiSettingsByAgent:
workspace.aiSettingsByAgent ??
Expand Down Expand Up @@ -2356,6 +2357,7 @@ export class Config {
runtimeConfig: workspace.runtimeConfig ?? DEFAULT_RUNTIME_CONFIG,
aiSettings: workspace.aiSettings,
heartbeat: workspace.heartbeat,
githubReviewNotificationsEnabled: workspace.githubReviewNotificationsEnabled,
goalDefaults: workspace.goalDefaults,
aiSettingsByAgent:
workspace.aiSettingsByAgent ??
Expand Down Expand Up @@ -2424,6 +2426,7 @@ export class Config {
runtimeConfig: workspace.runtimeConfig ?? DEFAULT_RUNTIME_CONFIG,
aiSettings: workspace.aiSettings,
heartbeat: workspace.heartbeat,
githubReviewNotificationsEnabled: workspace.githubReviewNotificationsEnabled,
goalDefaults: workspace.goalDefaults,
aiSettingsByAgent:
workspace.aiSettingsByAgent ??
Expand Down Expand Up @@ -2529,6 +2532,7 @@ export class Config {
runtimeConfig: metadata.runtimeConfig,
aiSettings: metadata.aiSettings,
heartbeat: metadata.heartbeat,
githubReviewNotificationsEnabled: metadata.githubReviewNotificationsEnabled,
goalDefaults: metadata.goalDefaults,
parentWorkspaceId: metadata.parentWorkspaceId,
agentType: metadata.agentType,
Expand Down Expand Up @@ -2599,14 +2603,19 @@ export class Config {
*/
async updateWorkspaceMetadata(
workspaceId: string,
updates: Partial<Pick<WorkspaceMetadata, "name" | "runtimeConfig">>
updates: Partial<
Pick<WorkspaceMetadata, "name" | "runtimeConfig" | "githubReviewNotificationsEnabled">
>
): Promise<void> {
await this.editConfig((config) => {
for (const [_projectPath, projectConfig] of config.projects) {
const workspace = projectConfig.workspaces.find((w) => w.id === workspaceId);
if (workspace) {
if (updates.name !== undefined) workspace.name = updates.name;
if (updates.runtimeConfig !== undefined) workspace.runtimeConfig = updates.runtimeConfig;
if (updates.githubReviewNotificationsEnabled !== undefined) {
workspace.githubReviewNotificationsEnabled = updates.githubReviewNotificationsEnabled;
}
return config;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/node/orpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4242,6 +4242,17 @@ export const router = (authToken?: string) => {
return Ok(undefined);
}),
},
githubReviewNotifications: {
set: t
.input(schemas.workspace.githubReviewNotifications.set.input)
.output(schemas.workspace.githubReviewNotifications.set.output)
.handler(({ context, input }) =>
context.workspaceService.setGitHubReviewNotificationsEnabled(
input.workspaceId,
input.enabled
)
),
},
goalDefaults: {
// Per-workspace override of the global `goalDefaults` block.
// `get` returns `null` when this workspace has no override.
Expand Down
Loading
Loading