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
21 changes: 15 additions & 6 deletions packages/agent/src/server/agent-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { type ServerType, serve } from "@hono/node-server";
import { execGh } from "@posthog/git/gh";
import { getCurrentBranch } from "@posthog/git/queries";
import { buildAdditionalDirectoriesPrompt } from "@posthog/shared";
import { unzipSync } from "fflate";
import { Hono } from "hono";
import { z } from "zod";
Expand Down Expand Up @@ -2481,11 +2482,12 @@ export class AgentServer {
slackThreadUrl?: string | null,
inboxReportUrl?: string | null,
): string | { append: string } {
const cloudAppend = this.buildCloudSystemPrompt(
prUrl,
slackThreadUrl,
inboxReportUrl,
);
const cloudAppend = [
this.buildCloudSystemPrompt(prUrl, slackThreadUrl, inboxReportUrl),
buildAdditionalDirectoriesPrompt(this.config.additionalDirectories),
]
.filter(Boolean)
.join("\n\n");
const userPrompt = this.config.claudeCode?.systemPrompt;

// String override: combine user prompt with cloud instructions
Expand Down Expand Up @@ -2523,8 +2525,12 @@ export class AgentServer {
const plugins = this.config.claudeCode?.plugins;
const effort =
runtimeAdapter === "claude" ? this.config.reasoningEffort : undefined;
const additionalDirectories =
runtimeAdapter === "claude"
? this.config.additionalDirectories
: undefined;

if (!plugins?.length && !effort) {
if (!plugins?.length && !effort && !additionalDirectories?.length) {
return undefined;
}

Expand All @@ -2535,6 +2541,9 @@ export class AgentServer {
if (effort) {
options.effort = effort;
}
if (additionalDirectories?.length) {
options.additionalDirectories = additionalDirectories;
}
return { claudeCode: { options } };
}

Expand Down
12 changes: 12 additions & 0 deletions packages/agent/src/server/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ program
"--allowedDomains <domains>",
"Comma-separated list of domains allowed for web tools (WebFetch, WebSearch)",
)
.option(
"--additionalDirectories <paths>",
"Comma-separated extra repo checkout paths the agent may read/edit beyond --repositoryPath",
)
.action(async (options) => {
const envResult = envSchema.safeParse(process.env);

Expand Down Expand Up @@ -149,6 +153,13 @@ program
.filter(Boolean)
: undefined;

const additionalDirectories = options.additionalDirectories
? options.additionalDirectories
.split(",")
.map((d: string) => d.trim())
.filter(Boolean)
: undefined;

if (
env.POSTHOG_CODE_RUNTIME_ADAPTER &&
env.POSTHOG_CODE_MODEL &&
Expand Down Expand Up @@ -186,6 +197,7 @@ program
baseBranch: options.baseBranch,
claudeCode,
allowedDomains,
additionalDirectories,
runtimeAdapter: env.POSTHOG_CODE_RUNTIME_ADAPTER,
model: env.POSTHOG_CODE_MODEL,
reasoningEffort: env.POSTHOG_CODE_REASONING_EFFORT,
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface AgentServerConfig {
mcpServers?: RemoteMcpServer[];
baseBranch?: string;
claudeCode?: ClaudeCodeConfig;
additionalDirectories?: string[];
allowedDomains?: string[];
runtimeAdapter?: "claude" | "codex";
model?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/api-client/src/posthog-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2220,6 +2220,7 @@ export class PostHogAPIClient {
channel?: string | null;
pending_user_message?: string;
pending_user_artifact_ids?: string[];
additional_repositories?: string[];
},
) {
const teamId = await this.getTeamId();
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/task-detail/taskCreationSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,11 @@ export class TaskCreationSaga extends Saga<
input.workspaceMode === "cloud"
? (input.reasoningLevel ?? null)
: undefined,
additional_repositories:
input.workspaceMode === "cloud" &&
input.additionalRepositories?.length
? input.additionalRepositories
: undefined,
signal_report: input.signalReportId ?? undefined,
channel: input.channelId ?? undefined,
pending_user_message: warmPayload?.pendingUserMessage,
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/task-detail/taskInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface PrepareTaskInputOptions {
sandboxEnvironmentId?: string;
signalReportId?: string;
additionalDirectories?: string[];
additionalRepositories?: string[];
channelContext?: string;
channelName?: string;
channelId?: string;
Expand Down Expand Up @@ -58,6 +59,9 @@ export function prepareTaskInput(
options.signalReportId && isCloud ? "signal_report" : undefined,
signalReportId: options.signalReportId,
additionalDirectories: isCloud ? undefined : options.additionalDirectories,
additionalRepositories: isCloud
? options.additionalRepositories
: undefined,
channelContext: options.channelContext,
channelName: options.channelName,
channelId: options.channelId,
Expand Down
32 changes: 32 additions & 0 deletions packages/shared/src/agent-prompts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { buildAdditionalDirectoriesPrompt } from "./agent-prompts";

describe("buildAdditionalDirectoriesPrompt", () => {
it.each([
["undefined", undefined],
["empty", []],
])("returns an empty string for %s directories", (_label, directories) => {
expect(buildAdditionalDirectoriesPrompt(directories)).toBe("");
});

it("lists each directory in an <additional_directories> block", () => {
const prompt = buildAdditionalDirectoriesPrompt([
"/tmp/workspace/repos/posthog/posthog-js",
"/tmp/workspace/repos/posthog/posthog.com",
]);

expect(prompt).toContain(
"<additional_directories>\n" +
" <directory>/tmp/workspace/repos/posthog/posthog-js</directory>\n" +
" <directory>/tmp/workspace/repos/posthog/posthog.com</directory>\n" +
"</additional_directories>",
);
});

it("escapes XML-significant characters in paths", () => {
const prompt = buildAdditionalDirectoriesPrompt(["/tmp/a<b>&c"]);

expect(prompt).toContain("<directory>/tmp/a&lt;b&gt;&amp;c</directory>");
expect(prompt).not.toContain("<directory>/tmp/a<b>");
});
});
11 changes: 11 additions & 0 deletions packages/shared/src/agent-prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function buildAdditionalDirectoriesPrompt(
directories: readonly string[] | undefined,
): string {
if (!directories?.length) return "";
const escapeXml = (value: string) =>
value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const dirs = directories
.map((directory) => ` <directory>${escapeXml(directory)}</directory>`)
.join("\n");
return `The user has granted you access to additional directories outside the working directory. You may read and edit files in these paths just like the working directory:\n<additional_directories>\n${dirs}\n</additional_directories>`;
}
1 change: 1 addition & 0 deletions packages/shared/src/domain-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface Task {
created_by?: UserBasic | null;
origin_product: string;
repository?: string | null; // Format: "organization/repository" (e.g., "posthog/posthog-js")
additional_repositories?: string[];
github_integration?: number | null;
github_user_integration?: string | null;
json_schema?: Record<string, unknown> | null;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { buildAdditionalDirectoriesPrompt } from "./agent-prompts";
export * from "./analytics-events";
export { type ArchivedTask, archivedTaskSchema } from "./archive-domain";
export { withTimeout } from "./async";
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/task-creation-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface TaskCreationInput {
filePaths?: string[];
repoPath?: string;
repository?: string | null;
additionalRepositories?: string[];
workspaceMode?: WorkspaceMode;
branch?: string | null;
// When the branch exists only on the remote, opt in to fetching and checking
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { GithubLogo, Plus } from "@phosphor-icons/react";
import {
Button,
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from "@posthog/quill";
import { type RefObject, useMemo, useRef } from "react";

interface AdditionalRepositoriesButtonProps {
values: string[];
onChange: (values: string[]) => void;
repositories: string[];
primaryRepository?: string | null;
disabled?: boolean;
anchor?: RefObject<HTMLElement | null>;
}

const VISIBLE_LIMIT = 50;

export function AdditionalRepositoriesButton({
values,
onChange,
repositories,
primaryRepository,
disabled = false,
anchor,
}: AdditionalRepositoriesButtonProps) {
const triggerRef = useRef<HTMLButtonElement>(null);

const selectableRepos = useMemo(
() => repositories.filter((repo) => repo !== primaryRepository),
[repositories, primaryRepository],
);

const selectedValues = useMemo(
() => values.filter((repo) => repo !== primaryRepository),
[values, primaryRepository],
);
const count = selectedValues.length;

if (selectableRepos.length === 0) return null;

return (
<Combobox
items={selectableRepos}
limit={VISIBLE_LIMIT}
multiple
value={selectedValues}
onValueChange={(next: string[]) =>
onChange(next.filter((repo) => repo !== primaryRepository))
}
disabled={disabled}
>
<ComboboxTrigger
render={
<Button
ref={triggerRef}
variant="outline"
size="sm"
disabled={disabled}
aria-label="Additional repositories"
>
<GithubLogo size={14} weight="regular" className="shrink-0" />
{count > 0 ? (
<span className="font-medium tabular-nums">+{count}</span>
) : (
<Plus size={12} weight="bold" className="shrink-0" />
)}
</Button>
}
/>
<ComboboxContent
anchor={anchor ?? triggerRef}
side="bottom"
sideOffset={6}
align="start"
className="min-w-[280px]"
>
<ComboboxInput placeholder="Add repositories..." />
<ComboboxEmpty>No repositories found.</ComboboxEmpty>
<ComboboxList>
{(repo: string) => (
<ComboboxItem key={repo} value={repo}>
{repo}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { GithubLogo } from "@phosphor-icons/react";
import type { Task } from "@posthog/shared/domain-types";
import { Tooltip } from "../../../primitives/Tooltip";

export function AdditionalReposBadge({ task }: { task: Task }) {
const extras = task.additional_repositories ?? [];
if (extras.length === 0) return null;

return (
<Tooltip
side="bottom"
content={
<div className="flex flex-col gap-1">
<span className="text-(--gray-11)">Repositories in this task</span>
{task.repository && (
<span className="font-mono">{task.repository}</span>
)}
{extras.map((repo) => (
<span key={repo} className="font-mono">
{repo}
</span>
))}
</div>
}
>
<span className="no-drag flex h-[24px] shrink-0 items-center gap-1 rounded-md border border-(--gray-6) px-2 font-mono text-(--gray-11) text-[11px]">
<GithubLogo size={12} className="shrink-0" />+{extras.length}
</span>
</Tooltip>
);
}
23 changes: 23 additions & 0 deletions packages/ui/src/features/task-detail/components/TaskInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useHostTRPC, useHostTRPCClient } from "@posthog/host-router/react";
import { ButtonGroup } from "@posthog/quill";
import { ANALYTICS_EVENTS } from "@posthog/shared";
import type { Task } from "@posthog/shared/domain-types";
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
import { openSettings } from "@posthog/ui/features/settings/hooks/useOpenSettings";
import type { TaskInputReportAssociation } from "@posthog/ui/features/task-detail/stores/taskInputPrefillStore";
import { useTaskInputPrefillStore } from "@posthog/ui/features/task-detail/stores/taskInputPrefillStore";
Expand Down Expand Up @@ -38,6 +39,7 @@ import { useFileSearchStore } from "../../command/fileSearchStore";
import { NewTaskFilePreview } from "../../command/NewTaskFilePreview";
import { EnvironmentSelector } from "../../environments/EnvironmentSelector";
import { AdditionalDirectoriesButton } from "../../folder-picker/AdditionalDirectoriesButton";
import { AdditionalRepositoriesButton } from "../../folder-picker/AdditionalRepositoriesButton";
import { FolderPicker } from "../../folder-picker/FolderPicker";
import { GitHubRepoPicker } from "../../folder-picker/GitHubRepoPicker";
import { useFolders } from "../../folders/useFolders";
Expand Down Expand Up @@ -147,6 +149,9 @@ export function TaskInput({
onContextChipClick,
}: TaskInputProps = {}) {
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
// Dark-launch gate for the cloud multi-repo picker. Always on in dev.
const cloudMultiRepoEnabled =
useFeatureFlag("posthog-code-cloud-multi-repo") || import.meta.env.DEV;
const trpc = useHostTRPC();
const hostClient = useHostTRPCClient();
const gitWriteClient = useMemo(
Expand Down Expand Up @@ -349,6 +354,9 @@ export function TaskInput({
const lower = selectedRepository.toLowerCase();
return repositories.includes(lower) ? lower : null;
}, [selectedRepository, repositories]);
const [additionalRepositories, setAdditionalRepositories] = useState<
string[]
>([]);
const { currentBranch, branchLoading, defaultBranch, busyState } =
useGitQueries(selectedDirectory);

Expand Down Expand Up @@ -785,6 +793,9 @@ export function TaskInput({
signalReportId: activeReportAssociation?.reportId,
channelContext: includeChannelContext ? channelContext : undefined,
channelName,
additionalRepositories: additionalRepositories.filter(
(repo) => repo !== selectedCloudRepository,
),
allowNoRepo,
});

Expand Down Expand Up @@ -1129,6 +1140,18 @@ export function TaskInput({
disabled={isCreatingTask}
/>
)}
{!allowNoRepo &&
workspaceMode === "cloud" &&
cloudMultiRepoEnabled &&
selectedCloudRepository !== null && (
<AdditionalRepositoriesButton
values={additionalRepositories}
onChange={setAdditionalRepositories}
repositories={repositories}
primaryRepository={selectedCloudRepository}
disabled={isCreatingTask}
/>
)}
{cloudRegion === "dev" && (
<Flex align="center" gap="1" className="shrink-0">
<span
Expand Down
Loading
Loading