diff --git a/products/desktop/packages/core/src/task-detail/taskCreationSaga.ts b/products/desktop/packages/core/src/task-detail/taskCreationSaga.ts index bffba3e95a8e..5af70503414f 100644 --- a/products/desktop/packages/core/src/task-detail/taskCreationSaga.ts +++ b/products/desktop/packages/core/src/task-detail/taskCreationSaga.ts @@ -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: diff --git a/products/desktop/packages/core/src/task-detail/taskInput.test.ts b/products/desktop/packages/core/src/task-detail/taskInput.test.ts index e30a5a4c3902..1b4c8f0aba0a 100644 --- a/products/desktop/packages/core/src/task-detail/taskInput.test.ts +++ b/products/desktop/packages/core/src/task-detail/taskInput.test.ts @@ -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", () => { diff --git a/products/desktop/packages/core/src/task-detail/taskInput.ts b/products/desktop/packages/core/src/task-detail/taskInput.ts index 64544f9f3297..8a0e5f246bc7 100644 --- a/products/desktop/packages/core/src/task-detail/taskInput.ts +++ b/products/desktop/packages/core/src/task-detail/taskInput.ts @@ -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; @@ -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, diff --git a/products/desktop/packages/shared/src/task-creation-domain.ts b/products/desktop/packages/shared/src/task-creation-domain.ts index a3e9ca4644b8..bd4ca4230518 100644 --- a/products/desktop/packages/shared/src/task-creation-domain.ts +++ b/products/desktop/packages/shared/src/task-creation-domain.ts @@ -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 diff --git a/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index d92522f564e6..f25aeaa53a30 100644 --- a/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -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"; @@ -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. */ @@ -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; @@ -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 @@ -76,6 +88,8 @@ export const ChannelHomeComposer = forwardRef< channelId, channelName, channelContext, + channelRepositories = [], + channelGithubIntegration = null, onTaskCreated, onPendingStart, onPendingEnd, @@ -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); @@ -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 @@ -474,9 +508,45 @@ export const ChannelHomeComposer = forwardRef< size="1" disabled={isBusy} /> + setRepositoryDialogOpen(true)} + /> )} + { + 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"), + }, + ); + } + }} + /> + 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 ( + + ); +} + +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 ( + + + + + {cloud ? "Add repositories" : "Select folder"} + + + {cloud + ? "Choose the repositories this task can work across." + : "Choose the local folder this task should work in."} + + + + {cloud ? ( +
+ { + setDraftRepositories(next); + setDraftIntegrationId(nextIntegrationId); + }} + /> + +
+ ) : ( + + )} +
+ + + + +
+
+ ); +} diff --git a/products/desktop/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx b/products/desktop/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx index 293a71d55874..f52c8e478368 100644 --- a/products/desktop/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx +++ b/products/desktop/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx @@ -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} diff --git a/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.test.tsx b/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.test.tsx index 45babfd347f6..9d90701c7b37 100644 --- a/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.test.tsx +++ b/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.test.tsx @@ -35,6 +35,22 @@ vi.mock("@posthog/ui/features/task-detail/components/TaskInput", () => ({ }, })); +// The raw channel rows WebsiteNewTask reads repository defaults from. +vi.mock("@posthog/ui/features/canvas/hooks/useTaskChannels", () => ({ + useTaskChannels: () => ({ + channels: [ + { + id: "chan-1", + name: "project-bluebird", + channel_type: "public", + starred: false, + }, + ], + }), +})); + +// SpaceSelect (the spaceSelector chip) reads useChannels; keep its dependency +// chain inert under the stubbed TaskInput. vi.mock("@posthog/ui/features/canvas/hooks/useChannels", () => ({ useChannels: () => ({ channels: [ diff --git a/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx b/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx index 6b28e70cc1e2..29e5ed2967df 100644 --- a/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx +++ b/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx @@ -4,10 +4,10 @@ import { CHANNEL_TASK_SUGGESTIONS } from "@posthog/ui/features/canvas/channelTas import { ChannelBreadcrumb } from "@posthog/ui/features/canvas/components/ChannelBreadcrumb"; import { ChannelContextPanel } from "@posthog/ui/features/canvas/components/ChannelContextPanel"; import { SpaceSelect } from "@posthog/ui/features/canvas/components/SpaceSelect"; -import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; import { useFolderInstructions } from "@posthog/ui/features/canvas/hooks/useFolderInstructions"; +import { useTaskChannels } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; import { TaskInput } from "@posthog/ui/features/task-detail/components/TaskInput"; import { taskDetailQuery } from "@posthog/ui/features/tasks/queries"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; @@ -29,8 +29,10 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) { const view = useAppView(); const queryClient = useQueryClient(); const { fileTask } = useChannelTaskMutations(); - const { channels } = useChannels(); - const channelName = channels.find((c) => c.id === channelId)?.name; + // The raw channel row also carries the space's repository defaults. + const { channels } = useTaskChannels(); + const channel = channels.find((c) => c.id === channelId); + const channelName = channel?.name; // Surface the channel breadcrumb in the shared header, same as the other // channel scenes ("# channel / New task"). @@ -145,6 +147,8 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) { channelId={channelId} channelContextId={channelId} allowNoRepo + channelRepositories={channel?.repositories} + channelGithubIntegration={channel?.github_integration} // So a prompt handed to openTaskInput survives routing into a channel. initialPrompt={view.initialPrompt} initialPromptKey={view.taskInputRequestId} diff --git a/products/desktop/packages/ui/src/features/canvas/stores/taskRepositoryDraftStore.test.ts b/products/desktop/packages/ui/src/features/canvas/stores/taskRepositoryDraftStore.test.ts new file mode 100644 index 000000000000..3045dcad8ce1 --- /dev/null +++ b/products/desktop/packages/ui/src/features/canvas/stores/taskRepositoryDraftStore.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { + resolveTaskRepositoryDraft, + useTaskRepositoryDraftStore, +} from "./taskRepositoryDraftStore"; + +describe("taskRepositoryDraftStore", () => { + beforeEach(() => { + useTaskRepositoryDraftStore.setState({ drafts: {} }); + }); + + it("falls back to the space defaults when no draft exists", () => { + const resolved = resolveTaskRepositoryDraft( + undefined, + ["posthog/posthog"], + 7, + ); + expect(resolved).toEqual({ + repositories: ["posthog/posthog"], + githubIntegration: 7, + folder: "", + }); + }); + + it("shares a draft set on one surface with every composer in the space", () => { + const { setDraft } = useTaskRepositoryDraftStore.getState(); + setDraft("chan-1", { + repositories: ["posthog/posthog-js"], + githubIntegration: 3, + folder: "", + }); + + const draft = useTaskRepositoryDraftStore.getState().drafts["chan-1"]; + expect(resolveTaskRepositoryDraft(draft, ["posthog/posthog"], 7)).toEqual({ + repositories: ["posthog/posthog-js"], + githubIntegration: 3, + folder: "", + }); + expect(useTaskRepositoryDraftStore.getState().drafts["chan-2"]).toBe( + undefined, + ); + }); + + it("falls back to the space defaults once the consumed draft is cleared", () => { + const { setDraft, clearDraft } = useTaskRepositoryDraftStore.getState(); + setDraft("chan-1", { + repositories: ["posthog/posthog-js"], + githubIntegration: 3, + folder: "", + }); + clearDraft("chan-1"); + + const draft = useTaskRepositoryDraftStore.getState().drafts["chan-1"]; + expect(resolveTaskRepositoryDraft(draft, ["posthog/posthog"], 7)).toEqual({ + repositories: ["posthog/posthog"], + githubIntegration: 7, + folder: "", + }); + }); + + it("keeps an emptied draft instead of backfilling from the defaults", () => { + const resolved = resolveTaskRepositoryDraft( + { repositories: [], githubIntegration: null, folder: "/tmp/work" }, + ["posthog/posthog"], + 7, + ); + expect(resolved.repositories).toEqual([]); + expect(resolved.githubIntegration).toBe(null); + expect(resolved.folder).toBe("/tmp/work"); + }); +}); diff --git a/products/desktop/packages/ui/src/features/canvas/stores/taskRepositoryDraftStore.ts b/products/desktop/packages/ui/src/features/canvas/stores/taskRepositoryDraftStore.ts new file mode 100644 index 000000000000..ac436afda938 --- /dev/null +++ b/products/desktop/packages/ui/src/features/canvas/stores/taskRepositoryDraftStore.ts @@ -0,0 +1,58 @@ +import { create } from "zustand"; + +/** A task-level repository/folder pick made in a composer's repository dialog. */ +export interface TaskRepositoryDraft { + repositories: string[]; + githubIntegration: number | null; + folder: string; +} + +interface TaskRepositoryDraftState { + /** Keyed by backend channel UUID, so every composer in a space shares one draft. */ + drafts: Record; + setDraft: (channelId: string, draft: TaskRepositoryDraft) => void; + /** Drop a consumed draft so the next task starts from the space defaults again. */ + clearDraft: (channelId: string) => void; +} + +// The next-task repository selection for each space. Held outside the +// composers so a pick made on one surface (space home, new-task screen) +// survives navigation and shows on the others, the way the prompt draft does. +// Like the prompt draft, it lives until the task it was picked for is created; +// the dialog's "save to space" checkbox is the durable path. +export const useTaskRepositoryDraftStore = create()( + (set) => ({ + drafts: {}, + setDraft: (channelId, draft) => + set((state) => ({ drafts: { ...state.drafts, [channelId]: draft } })), + clearDraft: (channelId) => + set((state) => { + if (!(channelId in state.drafts)) { + return state; + } + const { [channelId]: _dropped, ...drafts } = state.drafts; + return { drafts }; + }), + }), +); + +/** + * The selection a composer should show: the space draft when one exists, + * otherwise the space's saved defaults. An existing draft wins wholesale — + * an emptied repository list or cleared integration is a deliberate pick, + * not a gap to backfill from the defaults. + */ +export function resolveTaskRepositoryDraft( + draft: TaskRepositoryDraft | undefined, + channelRepositories: string[], + channelGithubIntegration: number | null, +): TaskRepositoryDraft { + if (draft) { + return draft; + } + return { + repositories: channelRepositories, + githubIntegration: channelGithubIntegration, + folder: "", + }; +} diff --git a/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx b/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx index d3da9b9a25b0..404d445465cb 100644 --- a/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -15,6 +15,15 @@ import { useHostTRPC, useHostTRPCClient } from "@posthog/host-router/react"; import { ButtonGroup } from "@posthog/quill"; import { type AgentRuntime, ANALYTICS_EVENTS } from "@posthog/shared"; import type { Task } from "@posthog/shared/domain-types"; +import { + TaskRepositoryChip, + TaskRepositoryDialog, +} from "@posthog/ui/features/canvas/components/TaskRepositoryDialog"; +import { useUpdateTaskChannelRepositories } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; +import { + resolveTaskRepositoryDraft, + useTaskRepositoryDraftStore, +} from "@posthog/ui/features/canvas/stores/taskRepositoryDraftStore"; 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"; @@ -137,6 +146,8 @@ interface TaskInputProps { * needs a repo and attaches one lazily. */ allowNoRepo?: boolean; + channelRepositories?: string[]; + channelGithubIntegration?: number | null; /** * Channels new-task starter prompts. When provided, a column of suggestion * cards renders below the input while it's empty; clicking one fills the @@ -179,6 +190,8 @@ export function TaskInput({ channelId, channelContextId, allowNoRepo, + channelRepositories = [], + channelGithubIntegration = null, suggestions, onSuggestionSelect, onContextChipClick, @@ -204,6 +217,24 @@ export function TaskInput({ ); const selectedDirectory = useActiveRepoStore((s) => s.path); const setSelectedDirectory = useActiveRepoStore((s) => s.setPath); + const [repositoryDialogOpen, setRepositoryDialogOpen] = useState(false); + // "" only on channel-less repo-optional surfaces (none today); a real space + // id keys the draft shared with the space home composer. + const repositoryDraftKey = channelId ?? ""; + const repositoryDraft = useTaskRepositoryDraftStore( + (s) => s.drafts[repositoryDraftKey], + ); + const setRepositoryDraft = useTaskRepositoryDraftStore((s) => s.setDraft); + const { + repositories: taskRepositories, + githubIntegration: taskGithubIntegration, + folder: taskFolder, + } = resolveTaskRepositoryDraft( + repositoryDraft, + channelRepositories, + channelGithubIntegration, + ); + const updateChannelRepositories = useUpdateTaskChannelRepositories(); // Inline file preview opened from the command palette's file search. const previewFile = useFileSearchStore((s) => s.previewFile); const closePreviewFile = useFileSearchStore((s) => s.closePreview); @@ -944,8 +975,14 @@ export function TaskInput({ } = useTaskCreation({ editorRef, sessionId, - selectedDirectory, + selectedDirectory: allowNoRepo ? taskFolder : selectedDirectory, selectedRepository: selectedCloudRepository, + repositories: + allowNoRepo && workspaceMode === "cloud" ? taskRepositories : undefined, + githubIntegrationId: + allowNoRepo && workspaceMode === "cloud" + ? (taskGithubIntegration ?? undefined) + : undefined, githubUserIntegrationId: selectedGithubUserIntegrationId, workspaceMode: effectiveWorkspaceMode, branch: branchForTaskCreation, @@ -1239,6 +1276,15 @@ export function TaskInput({ onCustomImageChange={setSelectedCustomImageId} size="1" /> + {allowNoRepo && ( + setRepositoryDialogOpen(true)} + /> + )} {!allowNoRepo && workspaceMode === "worktree" && ( + {allowNoRepo && ( + { + setRepositoryDraft(repositoryDraftKey, { + repositories: selection.repositories, + githubIntegration: selection.integrationId, + folder: selection.folder, + }); + if ( + selection.saveToSpace && + channelId && + workspaceMode === "cloud" + ) { + updateChannelRepositories.mutate( + { + channelId, + githubIntegration: selection.integrationId, + repositories: selection.repositories, + }, + { + onError: () => + toast.error("Couldn't save repositories to the space"), + }, + ); + } + }} + /> + )} + { diff --git a/products/desktop/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/products/desktop/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 9bbfaf88cd16..b246b6618b7d 100644 --- a/products/desktop/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/products/desktop/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -21,6 +21,7 @@ import { } from "@posthog/shared"; import type { ExecutionMode, Task } from "@posthog/shared/domain-types"; import { useTaskChannels } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; +import { useTaskRepositoryDraftStore } from "@posthog/ui/features/canvas/stores/taskRepositoryDraftStore"; import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; import { useTaskInputPrefillStore } from "@posthog/ui/features/task-detail/stores/taskInputPrefillStore"; import { navigateToTaskPending } from "@posthog/ui/router/navigationBridge"; @@ -70,6 +71,7 @@ interface UseTaskCreationOptions { sessionId: string; selectedDirectory: string; selectedRepository?: string | null; + repositories?: string[]; githubIntegrationId?: number; githubUserIntegrationId?: string; workspaceMode: WorkspaceMode; @@ -174,6 +176,7 @@ export function useTaskCreation({ sessionId, selectedDirectory, selectedRepository, + repositories, githubIntegrationId, githubUserIntegrationId, workspaceMode, @@ -364,10 +367,11 @@ export function useTaskCreation({ adapter, ); const input = prepareTaskInput(serializedContent, filePaths, { - // In channels chat-box mode no repo is attached up front, even if a - // directory/repo is lingering in the persisted picker state. - selectedDirectory: allowNoRepo ? undefined : selectedDirectory, + // Repo-optional surfaces may still supply an explicit task folder or + // repository selection; otherwise creation falls back to scratch. + selectedDirectory: selectedDirectory || undefined, selectedRepository: allowNoRepo ? null : selectedRepository, + repositories, githubIntegrationId, githubUserIntegrationId, workspaceMode, @@ -491,6 +495,12 @@ export function useTaskCreation({ if (!contentOverride) { useDraftStore.getState().actions.setDraft(sessionId, null); } + // The task-level repository/folder pick is consumed by this task; + // the next one starts from the space defaults again ("save to + // space" is the durable path). + if (allowNoRepo && channelId) { + useTaskRepositoryDraftStore.getState().clearDraft(channelId); + } void trackTaskCreated(input, selectedDirectory, hostClient); // Repo-less channel tasks create no workspace row (the agent runs in // a scratch dir surfaced as a synthetic workspace), so the normal @@ -547,6 +557,7 @@ export function useTaskCreation({ sessionId, selectedDirectory, selectedRepository, + repositories, githubIntegrationId, githubUserIntegrationId, workspaceMode,