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
136 changes: 110 additions & 26 deletions apps/code/src/renderer/features/task-detail/components/TaskInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping";
import { useConnectivity } from "@hooks/useConnectivity";
import {
useGithubBranches,
useGithubRepositories,
useRepositoryIntegration,
useUserGithubBranches,
useUserGithubRepositories,
useUserRepositoryIntegration,
Expand Down Expand Up @@ -175,20 +178,53 @@ export function TaskInput({
const setAdapter = (newAdapter: AgentAdapter) =>
setLastUsedAdapter(newAdapter);

const userRepositoryIntegration = useUserRepositoryIntegration();
const teamRepositoryIntegration = useRepositoryIntegration();
const useTeamGithubFallback =
!userRepositoryIntegration.isLoadingRepos &&
!userRepositoryIntegration.hasGithubIntegration;
const repositories = useTeamGithubFallback
? teamRepositoryIntegration.repositories
: userRepositoryIntegration.repositories;
const isLoadingRepos =
userRepositoryIntegration.isLoadingRepos ||
(useTeamGithubFallback && teamRepositoryIntegration.isLoadingRepos);
const isRefreshingRepos = useTeamGithubFallback
? teamRepositoryIntegration.isRefreshingRepos
: userRepositoryIntegration.isRefreshingRepos;
const refreshRepositories = useTeamGithubFallback
? teamRepositoryIntegration.refreshRepositories
: userRepositoryIntegration.refreshRepositories;
const {
repositories,
getInstallationIdForRepo,
getUserIntegrationIdForRepo,
isLoadingRepos,
isRefreshingRepos,
refreshRepositories,
} = useUserRepositoryIntegration();
repositories: visibleUserCloudRepositories,
isPending: userCloudRepositoriesLoading,
hasMore: userCloudRepositoriesHasMore,
loadMore: loadMoreUserCloudRepositories,
} = useUserGithubRepositories(
cloudRepoSearchQuery,
isCloudRepoPickerOpen && !useTeamGithubFallback,
);
const {
repositories: visibleCloudRepositories,
isPending: cloudRepositoriesLoading,
hasMore: cloudRepositoriesHasMore,
loadMore: loadMoreCloudRepositories,
} = useUserGithubRepositories(cloudRepoSearchQuery, isCloudRepoPickerOpen);
repositories: visibleTeamCloudRepositories,
isPending: teamCloudRepositoriesLoading,
hasMore: teamCloudRepositoriesHasMore,
loadMore: loadMoreTeamCloudRepositories,
} = useGithubRepositories(
cloudRepoSearchQuery,
isCloudRepoPickerOpen && useTeamGithubFallback,
);
const visibleCloudRepositories = useTeamGithubFallback
? visibleTeamCloudRepositories
: visibleUserCloudRepositories;
const cloudRepositoriesLoading = useTeamGithubFallback
? teamCloudRepositoriesLoading
: userCloudRepositoriesLoading;
const cloudRepositoriesHasMore = useTeamGithubFallback
? teamCloudRepositoriesHasMore
: userCloudRepositoriesHasMore;
const loadMoreCloudRepositories = useTeamGithubFallback
? loadMoreTeamCloudRepositories
: loadMoreUserCloudRepositories;
const [selectedRepository, setSelectedRepository] = useState<string | null>(
() =>
initialCloudRepository?.toLowerCase() ??
Expand All @@ -203,27 +239,74 @@ export function TaskInput({
const { currentBranch, branchLoading, defaultBranch } =
useGitQueries(selectedDirectory);

const selectedGithubUserIntegrationId = selectedCloudRepository
? getUserIntegrationIdForRepo(selectedCloudRepository)
: undefined;
const selectedInstallationId = selectedCloudRepository
? getInstallationIdForRepo(selectedCloudRepository)
: undefined;
const selectedGithubUserIntegrationId =
selectedCloudRepository && !useTeamGithubFallback
? userRepositoryIntegration.getUserIntegrationIdForRepo(
selectedCloudRepository,
)
: undefined;
const selectedGithubIntegrationId =
selectedCloudRepository && useTeamGithubFallback
? teamRepositoryIntegration.getIntegrationIdForRepo(
selectedCloudRepository,
)
: undefined;
const selectedInstallationId =
selectedCloudRepository && !useTeamGithubFallback
? userRepositoryIntegration.getInstallationIdForRepo(
selectedCloudRepository,
)
: undefined;

const {
data: cloudBranchData,
isPending: cloudBranchesLoading,
isRefreshing: cloudBranchesRefreshing,
isFetchingMore: cloudBranchesFetchingMore,
hasMore: cloudBranchesHasMore,
loadMore: loadMoreCloudBranches,
refresh: refreshCloudBranches,
data: userCloudBranchData,
isPending: userCloudBranchesLoading,
isRefreshing: userCloudBranchesRefreshing,
isFetchingMore: userCloudBranchesFetchingMore,
hasMore: userCloudBranchesHasMore,
loadMore: loadMoreUserCloudBranches,
refresh: refreshUserCloudBranches,
} = useUserGithubBranches(
selectedInstallationId,
selectedCloudRepository,
cloudBranchSearchQuery,
isCloudBranchPickerOpen,
isCloudBranchPickerOpen && !useTeamGithubFallback,
);
const {
data: teamCloudBranchData,
isPending: teamCloudBranchesLoading,
isRefreshing: teamCloudBranchesRefreshing,
isFetchingMore: teamCloudBranchesFetchingMore,
hasMore: teamCloudBranchesHasMore,
loadMore: loadMoreTeamCloudBranches,
refresh: refreshTeamCloudBranches,
} = useGithubBranches(
selectedGithubIntegrationId,
selectedCloudRepository,
cloudBranchSearchQuery,
isCloudBranchPickerOpen && useTeamGithubFallback,
);
const cloudBranchData = useTeamGithubFallback
? teamCloudBranchData
: userCloudBranchData;
const cloudBranchesLoading = useTeamGithubFallback
? teamCloudBranchesLoading
: userCloudBranchesLoading;
const cloudBranchesRefreshing = useTeamGithubFallback
? teamCloudBranchesRefreshing
: userCloudBranchesRefreshing;
const cloudBranchesFetchingMore = useTeamGithubFallback
? teamCloudBranchesFetchingMore
: userCloudBranchesFetchingMore;
const cloudBranchesHasMore = useTeamGithubFallback
? teamCloudBranchesHasMore
: userCloudBranchesHasMore;
const loadMoreCloudBranches = useTeamGithubFallback
? loadMoreTeamCloudBranches
: loadMoreUserCloudBranches;
const refreshCloudBranches = useTeamGithubFallback
? refreshTeamCloudBranches
: refreshUserCloudBranches;
const cloudBranches = cloudBranchData?.branches;
const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null;

Expand Down Expand Up @@ -432,6 +515,7 @@ export function TaskInput({
editorRef,
selectedDirectory,
selectedRepository: selectedCloudRepository,
githubIntegrationId: selectedGithubIntegrationId,
githubUserIntegrationId: selectedGithubUserIntegrationId,
workspaceMode: effectiveWorkspaceMode,
branch: branchForTaskCreation,
Expand Down
47 changes: 47 additions & 0 deletions apps/code/src/renderer/sagas/task/task-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,53 @@ describe("TaskCreationSaga", () => {
);
});

it("uses the team GitHub integration when no user GitHub integration is selected", async () => {
const createdTask = createTask({
github_integration: 42,
github_user_integration: null,
});
const startedTask = createTask({ latest_run: createRun() });
const createTaskMock = vi.fn().mockResolvedValue(createdTask);
const createTaskRunMock = vi.fn().mockResolvedValue(createRun());
const startTaskRunMock = vi.fn().mockResolvedValue(startedTask);

const saga = new TaskCreationSaga({
posthogClient: {
createTask: createTaskMock,
deleteTask: vi.fn(),
getTask: vi.fn(),
createTaskRun: createTaskRunMock,
startTaskRun: startTaskRunMock,
sendRunCommand: vi.fn(),
updateTask: vi.fn(),
} as never,
});

const result = await saga.run({
content: "Ship the fix",
repository: "posthog/posthog",
workspaceMode: "cloud",
branch: "main",
githubIntegrationId: 42,
});

expect(result.success).toBe(true);
expect(createTaskMock).toHaveBeenCalledWith(
expect.objectContaining({
repository: "posthog/posthog",
github_user_integration: undefined,
github_integration: 42,
}),
);
expect(createTaskRunMock).toHaveBeenCalledWith(
"task-123",
expect.objectContaining({
prAuthorshipMode: "bot",
runSource: "manual",
}),
);
});

it("uses user authorship for repo-less cloud tasks with a selected user GitHub integration", async () => {
const createdTask = createTask({
repository: null,
Expand Down
3 changes: 2 additions & 1 deletion apps/code/src/renderer/sagas/task/task-creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,8 @@ export class TaskCreationSaga extends Saga<
repository: repository ?? undefined,
github_integration:
input.workspaceMode === "cloud" &&
input.cloudRunSource === "signal_report"
(input.cloudRunSource === "signal_report" ||
!input.githubUserIntegrationId)
? input.githubIntegrationId
: undefined,
github_user_integration:
Expand Down
43 changes: 43 additions & 0 deletions packages/agent/src/server/agent-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,49 @@ describe("AgentServer HTTP Mode", () => {
expect(prompt).toContain("stop with local changes ready for review");
});

it.each([
{
label: "createPr unset",
config: { repositoryPath: undefined },
shouldContain: [
"Cloud Task Execution — No Repository Mode",
"Clone the repository into /tmp/workspace/repos/<owner>/<repo>",
"gh repo clone <owner>/<repo> /tmp/workspace/repos/<owner>/<repo>",
"If the user explicitly asks you to open or update a pull request",
"open a draft pull request",
"unless the user explicitly asks",
"Generated-By: PostHog Code",
"Task-Id: test-task-id",
],
shouldNotContain: [],
},
{
label: "createPr false",
config: { repositoryPath: undefined, createPr: false },
shouldContain: [
"Cloud Task Execution — No Repository Mode",
"You may clone a repository and make local edits in that clone",
"Do NOT create branches, commits, push changes, or open pull requests in this run",
],
shouldNotContain: ["open a draft pull request", "gh pr create --draft"],
},
])(
"returns no-repository prompt for $label",
({ config, shouldContain, shouldNotContain }) => {
const s = createServer(config);
const prompt = (
s as unknown as TestableServer
).buildCloudSystemPrompt();

for (const text of shouldContain) {
expect(prompt).toContain(text);
}
for (const text of shouldNotContain) {
expect(prompt).not.toContain(text);
}
},
);

it("returns auto-PR prompt for Slack-origin runs", () => {
process.env.POSTHOG_CODE_INTERACTION_ORIGIN = "slack";
const s = createServer();
Expand Down
18 changes: 16 additions & 2 deletions packages/agent/src/server/agent-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1582,6 +1582,19 @@ ${attributionInstructions}
}

if (!this.config.repositoryPath) {
const publishInstructions =
this.config.createPr === false
? `
When the user asks for code changes:
- You may clone a repository and make local edits in that clone
- Do NOT create branches, commits, push changes, or open pull requests in this run`
: `
When the user explicitly asks to clone or work in a GitHub repository:
- Clone the repository into /tmp/workspace/repos/<owner>/<repo> using \`gh repo clone <owner>/<repo> /tmp/workspace/repos/<owner>/<repo>\`
- Work from inside that cloned repository for follow-up code changes
- If the user explicitly asks you to open or update a pull request, create a branch, commit the requested changes, push it, and open a draft pull request from inside the clone
- Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`;

return `
# Cloud Task Execution — No Repository Mode

Expand All @@ -1594,11 +1607,12 @@ When the user asks about analytics, data, metrics, events, funnels, dashboards,

When the user asks for code changes or software engineering tasks:
- Let them know you can help but don't have a repository connected for this session
- Offer to write code snippets, scripts, or provide guidance
- If they have not specified a repository to clone, offer to write code snippets, scripts, or provide guidance
${publishInstructions}

Important:
- Do NOT create branches, commits, or pull requests in this mode.
- Prefer using MCP tools to answer questions with real data over giving generic advice.
${attributionInstructions}
`;
}

Expand Down
Loading