Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -800,10 +800,13 @@ export class TaskCreationSaga extends Saga<
input.runtime !== "pi" && !warmPayload?.suppressWarmReuse;
const result = await this.deps.posthogClient.createTask({
description,
repository: repository ?? undefined,
repository: input.repositories
? undefined
: (repository ?? undefined),
repositories: input.repositories,
github_integration:
input.workspaceMode === "cloud" &&
input.cloudRunSource === "signal_report"
(input.cloudRunSource === "signal_report" || input.repositories)
? input.githubIntegrationId
: undefined,
github_user_integration:
Expand Down
24 changes: 24 additions & 0 deletions products/desktop/packages/core/src/task-detail/taskInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ describe("prepareTaskInput", () => {
});
expect(input.customInstructions).toBeUndefined();
});

it("preserves task-specific cloud repositories", () => {
const input = prepareTaskInput("do the thing", [], {
workspaceMode: "cloud",
repositories: ["posthog/posthog", "posthog/posthog-js"],
githubIntegrationId: 42,
});

expect(input.repositories).toEqual([
"posthog/posthog",
"posthog/posthog-js",
]);
expect(input.githubIntegrationId).toBe(42);
});

it("uses a selected folder for a repo-optional local task", () => {
const input = prepareTaskInput("do the thing", [], {
workspaceMode: "local",
selectedDirectory: "/code/posthog",
allowNoRepo: true,
});

expect(input.repoPath).toBe("/code/posthog");
});
});

describe("buildWorktreeAdoptionInput", () => {
Expand Down
2 changes: 2 additions & 0 deletions products/desktop/packages/core/src/task-detail/taskInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ExecutionMode } from "@posthog/shared/domain-types";
export interface PrepareTaskInputOptions {
selectedDirectory?: string;
selectedRepository?: string | null;
repositories?: string[];
githubIntegrationId?: number;
githubUserIntegrationId?: string;
workspaceMode: WorkspaceMode;
Expand Down Expand Up @@ -56,6 +57,7 @@ export function prepareTaskInput(
filePaths,
repoPath: isCloud ? undefined : options.selectedDirectory,
repository: isCloud ? options.selectedRepository : undefined,
repositories: isCloud ? options.repositories : undefined,
githubIntegrationId: options.githubIntegrationId,
githubUserIntegrationId: options.githubUserIntegrationId,
workspaceMode: options.workspaceMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface TaskCreationInput {
filePaths?: string[];
repoPath?: string;
repository?: string | null;
repositories?: 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
Expand Up @@ -15,6 +15,7 @@ import {
useState,
} from "react";
import { useConnectivity } from "../../../hooks/useConnectivity";
import { toast } from "../../../primitives/toast";
import { track } from "../../../shell/analytics";
import { useFeatureFlag } from "../../feature-flags/useFeatureFlag";
import { useFeatureFlagsLoaded } from "../../feature-flags/useFeatureFlagsLoaded";
Expand Down Expand Up @@ -42,7 +43,16 @@ import { useTaskCreation } from "../../task-detail/hooks/useTaskCreation";
import { resolveWorkspaceModePreference } from "../../task-detail/hooks/workspaceModePreference";
import { channelFeedQueryKey } from "../hooks/useChannelFeed";
import { useGenerateFreeformCanvas } from "../hooks/useGenerateFreeformCanvas";
import { useUpdateTaskChannelRepositories } from "../hooks/useTaskChannels";
import {
resolveTaskRepositoryDraft,
useTaskRepositoryDraftStore,
} from "../stores/taskRepositoryDraftStore";
import type { PendingKickoff } from "./ChannelFeedView";
import {
TaskRepositoryChip,
TaskRepositoryDialog,
} from "./TaskRepositoryDialog";

export interface ChannelHomeComposerHandle {
/** Drop a starter prompt into the editor and apply its mode, if any. */
Expand All @@ -55,6 +65,8 @@ interface ChannelHomeComposerProps {
channelName?: string;
/** Channel CONTEXT.md, attached to the created task as background. */
channelContext?: string;
channelRepositories?: string[];
channelGithubIntegration?: number | null;
onTaskCreated: (task: Task) => void;
/** Post an optimistic kickoff to the feed the instant a submit is accepted. */
onPendingStart: (kickoff: PendingKickoff) => void;
Expand All @@ -64,10 +76,10 @@ interface ChannelHomeComposerProps {

// The prompt box at the bottom of a channel's homepage. A trimmed-down sibling
// of TaskInput: it reuses the same task-creation pipeline (model/mode/reasoning
// preview config + useTaskCreation) but drops the repo/branch pickers — channel
// tasks run repo-less and the agent attaches a repo lazily if it needs one. The
// starter-prompt suggestions render in the parent above the box; this owns the
// local/cloud selector.
// preview config + useTaskCreation) but drops the branch picker. Tasks default
// to the space's repositories; the chip beside the local/cloud selector swaps
// in a task-specific repository or folder selection. The starter-prompt
// suggestions render in the parent above the box; this owns the selector row.
export const ChannelHomeComposer = forwardRef<
ChannelHomeComposerHandle,
ChannelHomeComposerProps
Expand All @@ -76,6 +88,8 @@ export const ChannelHomeComposer = forwardRef<
channelId,
channelName,
channelContext,
channelRepositories = [],
channelGithubIntegration = null,
onTaskCreated,
onPendingStart,
onPendingEnd,
Expand Down Expand Up @@ -178,6 +192,21 @@ export const ChannelHomeComposer = forwardRef<
const [selectedCustomImageId, setSelectedCustomImageId] = useState<
string | null
>(null);
const [repositoryDialogOpen, setRepositoryDialogOpen] = useState(false);
const repositoryDraft = useTaskRepositoryDraftStore(
(s) => s.drafts[channelId],
);
const setRepositoryDraft = useTaskRepositoryDraftStore((s) => s.setDraft);
const {
repositories: taskRepositories,
githubIntegration: taskGithubIntegration,
folder: taskFolder,
} = resolveTaskRepositoryDraft(
repositoryDraft,
channelRepositories,
channelGithubIntegration,
);
const updateChannelRepositories = useUpdateTaskChannelRepositories();
const setWorkspaceMode = useCallback(
(mode: WorkspaceMode) => {
setWorkspaceModeState(mode);
Expand Down Expand Up @@ -322,7 +351,12 @@ export const ChannelHomeComposer = forwardRef<
const { isCreatingTask, canSubmit, handleSubmit } = useTaskCreation({
editorRef,
sessionId,
selectedDirectory: "",
selectedDirectory: taskFolder,
repositories: workspaceMode === "cloud" ? taskRepositories : undefined,
githubIntegrationId:
workspaceMode === "cloud"
? (taskGithubIntegration ?? undefined)
: undefined,
workspaceMode,
sandboxEnvironmentId:
workspaceMode === "cloud" && selectedCloudEnvId
Expand Down Expand Up @@ -474,9 +508,45 @@ export const ChannelHomeComposer = forwardRef<
size="1"
disabled={isBusy}
/>
<TaskRepositoryChip
cloud={workspaceMode === "cloud"}
repositoryCount={taskRepositories.length}
hasFolder={!!taskFolder}
disabled={isBusy}
onOpen={() => setRepositoryDialogOpen(true)}
/>
</div>
)}

<TaskRepositoryDialog
open={repositoryDialogOpen}
onOpenChange={setRepositoryDialogOpen}
cloud={workspaceMode === "cloud"}
repositories={taskRepositories}
integrationId={taskGithubIntegration}
folder={taskFolder}
onApply={(selection) => {
setRepositoryDraft(channelId, {
repositories: selection.repositories,
githubIntegration: selection.integrationId,
folder: selection.folder,
});
if (selection.saveToSpace && workspaceMode === "cloud") {
updateChannelRepositories.mutate(
{
channelId,
githubIntegration: selection.integrationId,
repositories: selection.repositories,
},
{
onError: () =>
toast.error("Couldn't save repositories to the space"),
},
);
}
}}
/>

<PromptInput
ref={editorRef}
sessionId={sessionId}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { FolderOpenIcon, GithubLogoIcon } from "@phosphor-icons/react";
import {
Button,
Checkbox,
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@posthog/quill";
import { FolderPicker } from "@posthog/ui/features/folder-picker/FolderPicker";
import { useState } from "react";
import { RepositoriesField } from "./RepositoriesField";

interface TaskRepositoryDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
cloud: boolean;
repositories: string[];
integrationId: number | null;
folder: string;
onApply: (selection: {
repositories: string[];
integrationId: number | null;
folder: string;
saveToSpace: boolean;
}) => void;
}

/**
* The task's repository (cloud) or folder (local) selection — a chip for the
* composer's selector row, drawn like the WorkspaceModeSelect beside it.
* Clicking it opens the TaskRepositoryDialog.
*/
export function TaskRepositoryChip({
cloud,
repositoryCount,
hasFolder,
disabled,
onOpen,
}: {
cloud: boolean;
repositoryCount: number;
hasFolder: boolean;
disabled: boolean;
onOpen: () => void;
}) {
const label = cloud
? repositoryCount > 0
? `${repositoryCount} ${repositoryCount === 1 ? "repository" : "repositories"}`
: "Add repositories…"
: hasFolder
? "Folder selected"
: "Select folder…";

return (
<Button
type="button"
variant="default"
size="sm"
disabled={disabled}
aria-label={cloud ? "Task repositories" : "Task folder"}
onClick={onOpen}
>
<span className="text-muted-foreground">
{cloud ? <GithubLogoIcon size={14} /> : <FolderOpenIcon size={14} />}
</span>
{label}
</Button>
);
}

export function TaskRepositoryDialog({
open,
onOpenChange,
cloud,
repositories,
integrationId,
folder,
onApply,
}: TaskRepositoryDialogProps) {
const [draftRepositories, setDraftRepositories] = useState(repositories);
const [draftIntegrationId, setDraftIntegrationId] = useState(integrationId);
const [draftFolder, setDraftFolder] = useState(folder);
const [saveToSpace, setSaveToSpace] = useState(false);

// Snapshot the current selection into the draft when the dialog opens,
// adjusted during render so prop churn while open can't clobber edits.
const [wasOpen, setWasOpen] = useState(open);
if (open !== wasOpen) {
setWasOpen(open);
if (open) {
setDraftRepositories(repositories);
setDraftIntegrationId(integrationId);
setDraftFolder(folder);
setSaveToSpace(false);
}
}

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
{cloud ? "Add repositories" : "Select folder"}
</DialogTitle>
<DialogDescription>
{cloud
? "Choose the repositories this task can work across."
: "Choose the local folder this task should work in."}
</DialogDescription>
</DialogHeader>
<DialogBody>
{cloud ? (
<div className="flex flex-col gap-4">
<RepositoriesField
selected={draftRepositories}
integrationId={draftIntegrationId}
onChange={(next, nextIntegrationId) => {
setDraftRepositories(next);
setDraftIntegrationId(nextIntegrationId);
}}
/>
<label
htmlFor="save-task-repositories-to-space"
className="flex cursor-pointer items-center gap-2 text-sm"
>
<Checkbox
id="save-task-repositories-to-space"
checked={saveToSpace}
onCheckedChange={(checked) =>
setSaveToSpace(checked === true)
}
/>
Use these repositories for the whole space
</label>
</div>
) : (
<FolderPicker
value={draftFolder}
onChange={setDraftFolder}
placeholder="Select folder…"
variant="field"
/>
)}
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant="primary"
disabled={cloud ? draftRepositories.length === 0 : !draftFolder}
onClick={() => {
onApply({
repositories: draftRepositories,
integrationId: draftIntegrationId,
folder: draftFolder,
saveToSpace,
});
onOpenChange(false);
}}
>
{cloud ? (
<GithubLogoIcon size={16} />
) : (
<FolderOpenIcon size={16} />
)}
Apply
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) {
channelId={channelId}
channelName={channelName}
channelContext={channelContext}
channelRepositories={channel?.repositories}
channelGithubIntegration={channel?.github_integration}
onTaskCreated={onTaskCreated}
onPendingStart={addPending}
onPendingEnd={removePending}
Expand Down
Loading
Loading