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
9 changes: 6 additions & 3 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2227,9 +2227,12 @@ export class PostHogAPIClient {
return (await response.json()) as SignalTeamConfig;
}

async updateSignalTeamConfig(updates: {
default_autostart_priority: string;
}): Promise<SignalTeamConfig> {
async updateSignalTeamConfig(
updates: Partial<{
default_autostart_priority: string;
autostart_base_branches: Record<string, string>;
}>,
): Promise<SignalTeamConfig> {
const teamId = await this.getTeamId();
const url = new URL(
`${this.api.baseUrl}/api/projects/${teamId}/signals/config/`,
Expand Down
13 changes: 12 additions & 1 deletion apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -109,6 +118,7 @@ export function useCreatePrReport({
executionMode: "auto",
adapter,
model,
branch: baseBranch,
reasoningLevel: settings.lastUsedReasoningEffort ?? undefined,
cloudPrAuthorshipMode: "user",
cloudRunSource: "signal_report",
Expand All @@ -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,
Expand Down Expand Up @@ -168,6 +178,7 @@ export function useCreatePrReport({
getUserIntegrationIdForRepo,
invalidateTasks,
navigateToTask,
teamConfig,
]);

return { createPrReport, isCreatingPr };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -502,6 +504,42 @@ export function useSignalSourceManager() {
[client, queryClient],
);

const handleUpdateAutostartBaseBranches = useCallback(
async (branches: Record<string, string>) => {
if (!client) return;

const queryKey = ["signals", "team-config"];
const previous = queryClient.getQueryData<SignalTeamConfig | null>(
queryKey,
);

if (previous) {
queryClient.setQueryData<SignalTeamConfig | null>(queryKey, {
...previous,
autostart_base_branches: branches,
});
}

try {
const fresh = await client.updateSignalTeamConfig({
autostart_base_branches: branches,
});
queryClient.setQueryData<SignalTeamConfig | null>(queryKey, fresh);
} catch (error: unknown) {
queryClient.setQueryData<SignalTeamConfig | null>(
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;
Expand Down Expand Up @@ -590,10 +628,12 @@ export function useSignalSourceManager() {
evaluationsUrl,
handleToggleEvaluation,
teamConfig,
teamConfigLoading,
handleUpdateAutostartPriority,
userAutonomyConfig,
userAutonomyConfigLoading,
handleUpdateUserAutonomyPriority,
handleUpdateAutostartBaseBranches,
handleUpdateSlackNotifications,
};
}
Original file line number Diff line number Diff line change
@@ -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<string, string>;
/** Persist the full next mapping (the caller diffs/optimistically updates). */
onChange: (next: Record<string, string>) => 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<string | null>(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 (
<Flex
direction="column"
gap="2"
pt="3"
style={{ borderTop: "1px dashed var(--gray-5)" }}
>
<Flex direction="column" gap="1">
<Text className="font-medium text-(--gray-12) text-sm">
Base branch for auto-PRs
</Text>
<Text className="text-(--gray-11) text-[13px]">
Point auto-started inbox PRs at a specific branch per repository.
Repositories without an override target their default branch.
</Text>
</Flex>

{isLoading ? (
<Box className="h-[32px] w-[320px] animate-pulse rounded bg-gray-3" />
) : (
<Flex direction="column" gap="2">
{entries.map(([repo, branch]) => (
<BaseBranchRow
key={repo}
repo={repo}
value={branch}
integrationId={getIntegrationIdForRepo(repo)}
disabled={disabled}
onCommit={commit}
onRemove={remove}
/>
))}

<Flex align="center" gap="2">
<Box className="min-w-[220px] max-w-[280px]">
<GitHubRepoPicker
value={pendingRepo}
onChange={setPendingRepo}
repositories={selectableRepositories}
isLoading={
isLoadingRepos || (isRepoPickerOpen && repoPage.isPending)
}
isRefreshing={isRefreshingRepos}
onRefresh={refreshRepositories}
open={isRepoPickerOpen}
onOpenChange={setIsRepoPickerOpen}
searchQuery={repoSearch}
onSearchQueryChange={setRepoSearch}
hasMore={repoPage.hasMore}
onLoadMore={repoPage.loadMore}
disabled={disabled}
placeholder="Add a repository…"
size="2"
/>
</Box>
{pendingRepo ? (
<BaseBranchRow
key={`add-${pendingRepo}`}
repo={pendingRepo}
value={undefined}
integrationId={getIntegrationIdForRepo(pendingRepo)}
disabled={disabled}
onCommit={(repo, branch) => {
commit(repo, branch);
setPendingRepo(null);
setRepoSearch("");
}}
/>
) : null}
</Flex>
</Flex>
)}
</Flex>
);
}

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<string | null>(null);

const branchQuery = useGithubBranches(integrationId, repo, search, true);
const hasIntegrationId = !!integrationId;

const selectedBranch = isAdd ? draft : (value ?? null);

return (
<Flex align="center" gap="2">
{!isAdd ? (
<Text className="min-w-[220px] max-w-[280px] truncate text-(--gray-12) text-sm">
{repo}
</Text>
) : null}
<BranchSelector
repoPath={repo}
currentBranch={null}
defaultBranch={branchQuery.data?.defaultBranch ?? null}
workspaceMode="cloud"
disabled={disabled || !hasIntegrationId}
selectedBranch={selectedBranch}
onBranchSelect={(branch) => {
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 ? (
<Button
size="sm"
disabled={disabled || !draft}
onClick={() => {
if (draft) onCommit(repo, draft);
}}
>
Add
</Button>
) : (
<IconButton
variant="ghost"
color="gray"
aria-label={`Remove base branch override for ${repo}`}
disabled={disabled}
onClick={() => onRemove?.(repo)}
>
<X size={14} />
</IconButton>
)}
</Flex>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -46,6 +47,9 @@ export function SignalSourcesSettings({
userAutonomyConfig,
userAutonomyConfigLoading,
handleUpdateUserAutonomyPriority,
teamConfig,
teamConfigLoading,
handleUpdateAutostartBaseBranches,
} = useSignalSourceManager();

const { hasGithubIntegration, isLoadingIntegrations } =
Expand Down Expand Up @@ -135,6 +139,11 @@ export function SignalSourcesSettings({
/>
)}
</Flex>
<AutostartBaseBranchesSettings
branches={teamConfig?.autostart_base_branches ?? {}}
onChange={(next) => void handleUpdateAutostartBaseBranches(next)}
isLoading={teamConfigLoading}
/>
<SignalSlackNotificationsSettings
channelComboboxModal={slackNotificationsInModal}
isLoading={isLoadingIntegrations}
Expand Down
Loading
Loading