From a1fe16a7c88971c4b1a6123d7b0d7825bfc8aa94 Mon Sep 17 00:00:00 2001 From: Alessandro Pogliaghi Date: Thu, 4 Jun 2026 10:07:08 +0100 Subject: [PATCH] feat(inbox): configurable base branch for inbox auto-PRs chore: cleaner --- apps/code/src/renderer/api/posthogClient.ts | 9 +- .../features/inbox/hooks/useCreatePrReport.ts | 13 +- .../inbox/hooks/useSignalSourceManager.ts | 42 +++- .../AutostartBaseBranchesSettings.tsx | 219 ++++++++++++++++++ .../sections/SignalSourcesSettings.tsx | 9 + apps/code/src/shared/types.ts | 1 + 6 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 apps/code/src/renderer/features/settings/components/sections/AutostartBaseBranchesSettings.tsx diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 150b501901..1bbc56ba87 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -2227,9 +2227,12 @@ export class PostHogAPIClient { return (await response.json()) as SignalTeamConfig; } - async updateSignalTeamConfig(updates: { - default_autostart_priority: string; - }): Promise { + async updateSignalTeamConfig( + updates: Partial<{ + default_autostart_priority: string; + autostart_base_branches: Record; + }>, + ): Promise { const teamId = await this.getTeamId(); const url = new URL( `${this.api.baseUrl}/api/projects/${teamId}/signals/config/`, diff --git a/apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts b/apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts index 77203e8bfe..5d62860a53 100644 --- a/apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts +++ b/apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts @@ -18,6 +18,7 @@ import type { } from "../../task-detail/service/service"; import { buildCreatePrReportPrompt } from "../utils/buildCreatePrReportPrompt"; import { resolveDefaultModel } from "../utils/resolveDefaultModel"; +import { useSignalTeamConfig } from "./useSignalTeamConfig"; const log = logger.scope("create-pr-report"); @@ -52,6 +53,7 @@ export function useCreatePrReport({ const { getUserIntegrationIdForRepo } = useUserRepositoryIntegration(); const { invalidateTasks } = useCreateTask(); const cloudRegion = useAuthStateValue((state) => state.cloudRegion); + const { data: teamConfig } = useSignalTeamConfig(); const createPrReport = useCallback(async () => { if (isCreatingPr) return; @@ -100,6 +102,13 @@ export function useCreatePrReport({ return; } + const baseBranchOverrides = teamConfig?.autostart_base_branches ?? {}; + const targetRepo = cloudRepository.toLowerCase(); + const baseBranch = + Object.entries(baseBranchOverrides).find( + ([repo]) => repo.toLowerCase() === targetRepo, + )?.[1] ?? null; + const input: TaskCreationInput = { content: prompt, taskDescription: prompt, @@ -109,6 +118,7 @@ export function useCreatePrReport({ executionMode: "auto", adapter, model, + branch: baseBranch, reasoningLevel: settings.lastUsedReasoningEffort ?? undefined, cloudPrAuthorshipMode: "user", cloudRunSource: "signal_report", @@ -129,7 +139,7 @@ export function useCreatePrReport({ created_from: "command-menu", repository_provider: "github", workspace_mode: "cloud", - has_branch: false, + has_branch: baseBranch != null, cloud_run_source: "signal_report", cloud_pr_authorship_mode: "user", signal_report_id: reportId, @@ -168,6 +178,7 @@ export function useCreatePrReport({ getUserIntegrationIdForRepo, invalidateTasks, navigateToTask, + teamConfig, ]); return { createPrReport, isCreatingPr }; diff --git a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts index 64bbce120b..e4d774b93f 100644 --- a/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts +++ b/apps/code/src/renderer/features/inbox/hooks/useSignalSourceManager.ts @@ -7,6 +7,7 @@ import type { } from "@renderer/api/posthogClient"; import type { SignalReportPriority, + SignalTeamConfig, SignalUserAutonomyConfig, } from "@shared/types"; import { ANALYTICS_EVENTS } from "@shared/types/analytics"; @@ -113,7 +114,8 @@ export function useSignalSourceManager() { const { data: externalSources, isLoading: sourcesLoading } = useExternalDataSources(); const { data: evaluations } = useEvaluations(); - const { data: teamConfig } = useSignalTeamConfig(); + const { data: teamConfig, isLoading: teamConfigLoading } = + useSignalTeamConfig(); const { data: userAutonomyConfig, isLoading: userAutonomyConfigLoading } = useSignalUserAutonomyConfig(); @@ -502,6 +504,42 @@ export function useSignalSourceManager() { [client, queryClient], ); + const handleUpdateAutostartBaseBranches = useCallback( + async (branches: Record) => { + if (!client) return; + + const queryKey = ["signals", "team-config"]; + const previous = queryClient.getQueryData( + queryKey, + ); + + if (previous) { + queryClient.setQueryData(queryKey, { + ...previous, + autostart_base_branches: branches, + }); + } + + try { + const fresh = await client.updateSignalTeamConfig({ + autostart_base_branches: branches, + }); + queryClient.setQueryData(queryKey, fresh); + } catch (error: unknown) { + queryClient.setQueryData( + queryKey, + previous ?? null, + ); + const message = + error instanceof Error + ? error.message + : "Failed to update base branch setting"; + toast.error(message); + } + }, + [client, queryClient], + ); + const handleUpdateSlackNotifications = useCallback( async (updates: { integrationId?: number | null; @@ -590,10 +628,12 @@ export function useSignalSourceManager() { evaluationsUrl, handleToggleEvaluation, teamConfig, + teamConfigLoading, handleUpdateAutostartPriority, userAutonomyConfig, userAutonomyConfigLoading, handleUpdateUserAutonomyPriority, + handleUpdateAutostartBaseBranches, handleUpdateSlackNotifications, }; } diff --git a/apps/code/src/renderer/features/settings/components/sections/AutostartBaseBranchesSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/AutostartBaseBranchesSettings.tsx new file mode 100644 index 0000000000..e7c043ce85 --- /dev/null +++ b/apps/code/src/renderer/features/settings/components/sections/AutostartBaseBranchesSettings.tsx @@ -0,0 +1,219 @@ +import { GitHubRepoPicker } from "@features/folder-picker/components/GitHubRepoPicker"; +import { BranchSelector } from "@features/git-interaction/components/BranchSelector"; +import { + useGithubBranches, + useGithubRepositories, + useRepositoryIntegration, +} from "@hooks/useIntegrations"; +import { X } from "@phosphor-icons/react"; +import { Button } from "@posthog/quill"; +import { Box, Flex, IconButton, Text } from "@radix-ui/themes"; +import { useState } from "react"; + +interface AutostartBaseBranchesSettingsProps { + /** Current `org/repo` → base branch overrides. */ + branches: Record; + /** Persist the full next mapping (the caller diffs/optimistically updates). */ + onChange: (next: Record) => void; + isLoading?: boolean; +} + +/** + * Per-repository base branch overrides for auto-started inbox PRs. + * + * Each configured repo opens its auto-PRs against the chosen branch instead of + * the repo default. Repos without an entry keep targeting the repo default + */ +export function AutostartBaseBranchesSettings({ + branches, + onChange, + isLoading = false, +}: AutostartBaseBranchesSettingsProps) { + const { + repositories: allRepositories, + getIntegrationIdForRepo, + isLoadingRepos, + isRefreshingRepos, + refreshRepositories, + hasGithubIntegration, + } = useRepositoryIntegration(); + const disabled = !hasGithubIntegration; + + const [isRepoPickerOpen, setIsRepoPickerOpen] = useState(false); + const [repoSearch, setRepoSearch] = useState(""); + const [pendingRepo, setPendingRepo] = useState(null); + + const repoPage = useGithubRepositories(repoSearch, isRepoPickerOpen); + + const selectableRepositories = ( + isRepoPickerOpen ? repoPage.repositories : allRepositories + ).filter((repo) => !(repo in branches)); + + const entries = Object.entries(branches); + + const commit = (repo: string, branch: string) => { + onChange({ ...branches, [repo]: branch }); + }; + + const remove = (repo: string) => { + const next = { ...branches }; + delete next[repo]; + onChange(next); + }; + + return ( + + + + Base branch for auto-PRs + + + Point auto-started inbox PRs at a specific branch per repository. + Repositories without an override target their default branch. + + + + {isLoading ? ( + + ) : ( + + {entries.map(([repo, branch]) => ( + + ))} + + + + + + {pendingRepo ? ( + { + commit(repo, branch); + setPendingRepo(null); + setRepoSearch(""); + }} + /> + ) : null} + + + )} + + ); +} + +interface BaseBranchRowProps { + repo: string; + value: string | undefined; + integrationId: number | undefined; + disabled?: boolean; + onCommit: (repo: string, branch: string) => void; + onRemove?: (repo: string) => void; +} + +function BaseBranchRow({ + repo, + value, + integrationId, + disabled = false, + onCommit, + onRemove, +}: BaseBranchRowProps) { + const isAdd = value === undefined; + const [search, setSearch] = useState(""); + const [draft, setDraft] = useState(null); + + const branchQuery = useGithubBranches(integrationId, repo, search, true); + const hasIntegrationId = !!integrationId; + + const selectedBranch = isAdd ? draft : (value ?? null); + + return ( + + {!isAdd ? ( + + {repo} + + ) : null} + { + if (isAdd) { + setDraft(branch); + } else if (branch) { + onCommit(repo, branch); + } + }} + cloudBranches={branchQuery.data?.branches} + cloudBranchesLoading={branchQuery.isPending} + isRefreshing={branchQuery.isRefreshing} + cloudBranchesFetchingMore={branchQuery.isFetchingMore} + cloudBranchesHasMore={branchQuery.hasMore} + cloudSearchQuery={search} + onCloudSearchChange={setSearch} + onCloudLoadMore={branchQuery.loadMore} + /> + {isAdd ? ( + + ) : ( + onRemove?.(repo)} + > + + + )} + + ); +} diff --git a/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx index bfd4ce2e56..6d99cbea3d 100644 --- a/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/SignalSourcesSettings.tsx @@ -5,6 +5,7 @@ import { } from "@features/inbox/components/SignalSourceToggles"; import { useSignalSourceManager } from "@features/inbox/hooks/useSignalSourceManager"; import { SettingsOptionSelect } from "@features/settings/components/SettingsOptionSelect"; +import { AutostartBaseBranchesSettings } from "@features/settings/components/sections/AutostartBaseBranchesSettings"; import { GitHubIntegrationSection } from "@features/settings/components/sections/GitHubIntegrationSection"; import { SignalSlackNotificationsSettings } from "@features/settings/components/sections/SignalSlackNotificationsSettings"; import { useRepositoryIntegration } from "@hooks/useIntegrations"; @@ -46,6 +47,9 @@ export function SignalSourcesSettings({ userAutonomyConfig, userAutonomyConfigLoading, handleUpdateUserAutonomyPriority, + teamConfig, + teamConfigLoading, + handleUpdateAutostartBaseBranches, } = useSignalSourceManager(); const { hasGithubIntegration, isLoadingIntegrations } = @@ -135,6 +139,11 @@ export function SignalSourcesSettings({ /> )} + void handleUpdateAutostartBaseBranches(next)} + isLoading={teamConfigLoading} + /> | null; created_at: string; updated_at: string; }