diff --git a/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx b/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx
index 81ff7aad4..39eeb2270 100644
--- a/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx
+++ b/apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx
@@ -50,7 +50,6 @@ import type {
Task,
} from "@shared/types";
import type { InboxReportActionProperties } from "@shared/types/analytics";
-import { useNavigationStore } from "@stores/navigationStore";
import { useQuery } from "@tanstack/react-query";
import { isMac } from "@utils/platform";
import {
@@ -63,6 +62,7 @@ import {
useState,
} from "react";
import { toast } from "sonner";
+import { useCreatePrReport } from "../../hooks/useCreatePrReport";
import { useDiscussReport } from "../../hooks/useDiscussReport";
import { ReportImplementationPrLink } from "../utils/ReportImplementationPrLink";
import { SignalReportActionabilityBadge } from "../utils/SignalReportActionabilityBadge";
@@ -265,7 +265,6 @@ export function ReportDetailPane({
);
// ── Task creation ───────────────────────────────────────────────────────
- const { navigateToTaskInput } = useNavigationStore();
const { data: reportRepository } = useReportRepository(report.id);
const trpcReact = useTRPC();
const { data: mostRecentRepo } = useQuery(
@@ -360,22 +359,20 @@ export function ReportDetailPane({
[fireDetailAction],
);
- const handleCreateImplementationTask = useCallback(() => {
- if (!canCreateImplementationPr) return;
+ const { createPrReport, isCreatingPr } = useCreatePrReport({
+ reportId: report.id,
+ reportTitle: report.title,
+ cloudRepository: effectiveCloudRepository,
+ });
+
+ const handleCreateImplementationTask = useCallback(async () => {
+ if (!canCreateImplementationPr || isCreatingPr) return;
fireDetailAction("create_pr");
- navigateToTaskInput({
- initialPrompt: `Act on this signal report. Investigate the root cause, implement the fix, and open a PR if appropriate.\n\n${report.summary ?? ""}`,
- initialCloudRepository: effectiveCloudRepository ?? undefined,
- reportAssociation: {
- reportId: report.id,
- title: report.title ?? "Untitled signal",
- },
- });
+ await createPrReport();
}, [
canCreateImplementationPr,
- navigateToTaskInput,
- effectiveCloudRepository,
- report,
+ isCreatingPr,
+ createPrReport,
fireDetailAction,
]);
@@ -614,9 +611,10 @@ export function ReportDetailPane({
size="1"
variant="solid"
className="gap-1 text-[12px]"
+ disabled={isCreatingPr}
onClick={handleCreateImplementationTask}
>
-
+ {isCreatingPr ? : }
Create PR
diff --git a/apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts b/apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts
new file mode 100644
index 000000000..5ebf3f564
--- /dev/null
+++ b/apps/code/src/renderer/features/inbox/hooks/useCreatePrReport.ts
@@ -0,0 +1,173 @@
+import { useAuthStateValue } from "@features/auth/hooks/authQueries";
+import { useSettingsStore } from "@features/settings/stores/settingsStore";
+import { useCreateTask } from "@features/tasks/hooks/useTasks";
+import { useUserRepositoryIntegration } from "@hooks/useIntegrations";
+import { get } from "@renderer/di/container";
+import { RENDERER_TOKENS } from "@renderer/di/tokens";
+import { toast } from "@renderer/utils/toast";
+import { ANALYTICS_EVENTS } from "@shared/types/analytics";
+import { getCloudUrlFromRegion } from "@shared/utils/urls";
+import { useNavigationStore } from "@stores/navigationStore";
+import { track } from "@utils/analytics";
+import { logger } from "@utils/logger";
+import { useCallback, useState } from "react";
+import { toast as sonnerToast } from "sonner";
+import type {
+ TaskCreationInput,
+ TaskService,
+} from "../../task-detail/service/service";
+import { buildCreatePrReportPrompt } from "../utils/buildCreatePrReportPrompt";
+import { resolveDefaultModel } from "../utils/resolveDefaultModel";
+
+const log = logger.scope("create-pr-report");
+
+interface UseCreatePrReportOptions {
+ reportId: string;
+ reportTitle: string | null;
+ cloudRepository: string | null;
+}
+
+interface UseCreatePrReportReturn {
+ /** Create an auto-mode implementation task for the report and navigate to it on success. */
+ createPrReport: () => Promise;
+ /** True while the task is being created. */
+ isCreatingPr: boolean;
+}
+
+/**
+ * Create an implementation (PR) task directly from the inbox detail pane.
+ *
+ * Mirrors the Discuss flow: bypasses TaskInput so the user stays on the inbox
+ * until the task is ready, then jumps straight to the task detail page. The
+ * agent gets a short prompt that points it at the inbox MCP tools instead of
+ * inlining the entire report summary.
+ */
+export function useCreatePrReport({
+ reportId,
+ reportTitle,
+ cloudRepository,
+}: UseCreatePrReportOptions): UseCreatePrReportReturn {
+ const [isCreatingPr, setIsCreatingPr] = useState(false);
+ const { navigateToTask } = useNavigationStore();
+ const { getUserIntegrationIdForRepo } = useUserRepositoryIntegration();
+ const { invalidateTasks } = useCreateTask();
+ const cloudRegion = useAuthStateValue((state) => state.cloudRegion);
+
+ const createPrReport = useCallback(async () => {
+ if (isCreatingPr) return;
+ if (!cloudRepository) {
+ toast.error("Pick a cloud repository before creating a PR");
+ return;
+ }
+
+ const githubUserIntegrationId =
+ getUserIntegrationIdForRepo(cloudRepository);
+ if (!githubUserIntegrationId) {
+ toast.error("Connect a GitHub integration to create a PR");
+ return;
+ }
+
+ if (!cloudRegion) {
+ toast.error("Sign in to create a PR");
+ return;
+ }
+
+ setIsCreatingPr(true);
+ const toastId = toast.loading(
+ "Starting PR task...",
+ reportTitle ?? undefined,
+ );
+
+ const prompt = buildCreatePrReportPrompt({
+ reportId,
+ isDevBuild: import.meta.env.DEV,
+ });
+
+ const settings = useSettingsStore.getState();
+ const adapter = settings.lastUsedAdapter ?? "claude";
+ const apiHost = getCloudUrlFromRegion(cloudRegion);
+
+ const model =
+ settings.lastUsedModel ?? (await resolveDefaultModel(apiHost, adapter));
+
+ if (!model) {
+ sonnerToast.dismiss(toastId);
+ toast.error("Failed to start PR task", {
+ description:
+ "Couldn't resolve a default model. Open the task page once and pick a model, then try again.",
+ });
+ setIsCreatingPr(false);
+ return;
+ }
+
+ const input: TaskCreationInput = {
+ content: prompt,
+ taskDescription: prompt,
+ repository: cloudRepository,
+ githubUserIntegrationId,
+ workspaceMode: "cloud",
+ executionMode: "auto",
+ adapter,
+ model,
+ reasoningLevel: settings.lastUsedReasoningEffort ?? undefined,
+ cloudPrAuthorshipMode: "user",
+ cloudRunSource: "signal_report",
+ signalReportId: reportId,
+ };
+
+ try {
+ const taskService = get(RENDERER_TOKENS.TaskService);
+ const result = await taskService.createTask(input, (output) => {
+ invalidateTasks(output.task);
+ navigateToTask(output.task);
+ });
+
+ if (result.success) {
+ sonnerToast.dismiss(toastId);
+ track(ANALYTICS_EVENTS.TASK_CREATED, {
+ auto_run: true,
+ created_from: "command-menu",
+ repository_provider: "github",
+ workspace_mode: "cloud",
+ has_branch: false,
+ cloud_run_source: "signal_report",
+ cloud_pr_authorship_mode: "user",
+ adapter,
+ });
+ } else {
+ sonnerToast.dismiss(toastId);
+ toast.error("Failed to start PR task", {
+ description: result.error,
+ });
+ log.error("Create PR task creation failed", {
+ failedStep: result.failedStep,
+ error: result.error,
+ reportId,
+ reportTitle,
+ });
+ }
+ } catch (error) {
+ sonnerToast.dismiss(toastId);
+ const description =
+ error instanceof Error ? error.message : "Unknown error";
+ toast.error("Failed to start PR task", { description });
+ log.error("Unexpected error during Create PR task creation", {
+ error,
+ reportId,
+ });
+ } finally {
+ setIsCreatingPr(false);
+ }
+ }, [
+ isCreatingPr,
+ cloudRepository,
+ cloudRegion,
+ reportId,
+ reportTitle,
+ getUserIntegrationIdForRepo,
+ invalidateTasks,
+ navigateToTask,
+ ]);
+
+ return { createPrReport, isCreatingPr };
+}
diff --git a/apps/code/src/renderer/features/inbox/hooks/useDiscussReport.ts b/apps/code/src/renderer/features/inbox/hooks/useDiscussReport.ts
index b81a1e28c..7fdd66cde 100644
--- a/apps/code/src/renderer/features/inbox/hooks/useDiscussReport.ts
+++ b/apps/code/src/renderer/features/inbox/hooks/useDiscussReport.ts
@@ -4,7 +4,6 @@ import { useCreateTask } from "@features/tasks/hooks/useTasks";
import { useUserRepositoryIntegration } from "@hooks/useIntegrations";
import { get } from "@renderer/di/container";
import { RENDERER_TOKENS } from "@renderer/di/tokens";
-import { trpcClient } from "@renderer/trpc/client";
import { toast } from "@renderer/utils/toast";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { getCloudUrlFromRegion } from "@shared/utils/urls";
@@ -18,6 +17,7 @@ import type {
TaskService,
} from "../../task-detail/service/service";
import { buildDiscussReportPrompt } from "../utils/buildDiscussReportPrompt";
+import { resolveDefaultModel } from "../utils/resolveDefaultModel";
const log = logger.scope("discuss-report");
@@ -34,32 +34,6 @@ interface UseDiscussReportReturn {
isDiscussing: boolean;
}
-/**
- * Resolve the default model for the given adapter via the preview-config
- * tRPC query. Returns the server's `currentValue` for the `model` option, or
- * undefined if the call fails or the option is missing.
- */
-async function resolveDefaultModel(
- apiHost: string,
- adapter: "claude" | "codex",
-): Promise {
- try {
- const options = await trpcClient.agent.getPreviewConfigOptions.query({
- apiHost,
- adapter,
- });
- const modelOption = options.find(
- (o) => o.id === "model" || o.category === "model",
- );
- if (modelOption?.type === "select" && modelOption.currentValue) {
- return modelOption.currentValue;
- }
- } catch (error) {
- log.warn("Failed to resolve default model for Discuss", { error, adapter });
- }
- return undefined;
-}
-
/**
* Create a Discuss task directly from the inbox detail pane.
*
diff --git a/apps/code/src/renderer/features/inbox/utils/buildCreatePrReportPrompt.test.ts b/apps/code/src/renderer/features/inbox/utils/buildCreatePrReportPrompt.test.ts
new file mode 100644
index 000000000..5087a137d
--- /dev/null
+++ b/apps/code/src/renderer/features/inbox/utils/buildCreatePrReportPrompt.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from "vitest";
+import { buildCreatePrReportPrompt } from "./buildCreatePrReportPrompt";
+
+describe("buildCreatePrReportPrompt", () => {
+ it.each([
+ { isDevBuild: false, expectedScheme: "posthog-code" },
+ { isDevBuild: true, expectedScheme: "posthog-code-dev" },
+ ])(
+ "uses the $expectedScheme deeplink scheme when isDevBuild=$isDevBuild",
+ ({ isDevBuild, expectedScheme }) => {
+ const prompt = buildCreatePrReportPrompt({
+ reportId: "abc123",
+ isDevBuild,
+ });
+ expect(prompt).toContain(`${expectedScheme}://inbox/abc123`);
+ },
+ );
+
+ it("references the inbox MCP tools so the agent fetches the detail itself", () => {
+ const prompt = buildCreatePrReportPrompt({
+ reportId: "abc123",
+ isDevBuild: false,
+ });
+ expect(prompt).toContain("inbox MCP tools");
+ });
+
+ it("asks the agent to open a PR", () => {
+ const prompt = buildCreatePrReportPrompt({
+ reportId: "abc123",
+ isDevBuild: false,
+ });
+ expect(prompt).toMatch(/open a PR/i);
+ });
+
+ it("tells the agent to stop rather than guess if the report can't be fetched", () => {
+ const prompt = buildCreatePrReportPrompt({
+ reportId: "abc123",
+ isDevBuild: false,
+ });
+ expect(prompt).toMatch(/can't fetch the report/i);
+ expect(prompt).toMatch(/instead of guessing/i);
+ });
+});
diff --git a/apps/code/src/renderer/features/inbox/utils/buildCreatePrReportPrompt.ts b/apps/code/src/renderer/features/inbox/utils/buildCreatePrReportPrompt.ts
new file mode 100644
index 000000000..3e67772b0
--- /dev/null
+++ b/apps/code/src/renderer/features/inbox/utils/buildCreatePrReportPrompt.ts
@@ -0,0 +1,14 @@
+import { getDeeplinkProtocol } from "@shared/deeplink";
+
+interface BuildCreatePrReportPromptOptions {
+ reportId: string;
+ isDevBuild: boolean;
+}
+
+export function buildCreatePrReportPrompt({
+ reportId,
+ isDevBuild,
+}: BuildCreatePrReportPromptOptions): string {
+ const reportLink = `${getDeeplinkProtocol(isDevBuild)}://inbox/${reportId}`;
+ return `Act on PostHog inbox report ${reportId} ([inbox item](${reportLink})). Use the inbox MCP tools to fetch the report, its signals, and any suggested reviewers; investigate the root cause; implement the fix; and open a PR. If you can't fetch the report, stop and report that instead of guessing what it contains.`;
+}
diff --git a/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.test.ts b/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.test.ts
index 389e0dadc..a6e078a37 100644
--- a/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.test.ts
+++ b/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.test.ts
@@ -44,4 +44,18 @@ describe("buildDiscussReportPrompt", () => {
});
expect(prompt).toContain("brief readout");
});
+
+ it("tells the agent to say so rather than guess if the report can't be fetched", () => {
+ const withQuestion = buildDiscussReportPrompt({
+ reportId: "abc123",
+ question: "Why is conversion dropping?",
+ isDevBuild: false,
+ });
+ const withoutQuestion = buildDiscussReportPrompt({
+ reportId: "abc123",
+ isDevBuild: false,
+ });
+ expect(withQuestion).toMatch(/can't fetch the report/i);
+ expect(withoutQuestion).toMatch(/can't fetch the report/i);
+ });
});
diff --git a/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.ts b/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.ts
index bb4361402..fe815ca8f 100644
--- a/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.ts
+++ b/apps/code/src/renderer/features/inbox/utils/buildDiscussReportPrompt.ts
@@ -14,7 +14,10 @@ export function buildDiscussReportPrompt({
const trimmedQuestion = question?.trim();
const reportLink = `${getDeeplinkProtocol(isDevBuild)}://inbox/${reportId}`;
const intro = `Discuss PostHog inbox report ${reportId} ([inbox item](${reportLink})). Use the inbox MCP tools to fetch the report,`;
- return trimmedQuestion
+ const guard =
+ " If you can't fetch the report, say so instead of guessing what it contains.";
+ const body = trimmedQuestion
? `${intro} then answer this first: ${trimmedQuestion}`
: `${intro} then give me a brief readout and ask what I want to dig into.`;
+ return `${body}${guard}`;
}
diff --git a/apps/code/src/renderer/features/inbox/utils/resolveDefaultModel.ts b/apps/code/src/renderer/features/inbox/utils/resolveDefaultModel.ts
new file mode 100644
index 000000000..21690f976
--- /dev/null
+++ b/apps/code/src/renderer/features/inbox/utils/resolveDefaultModel.ts
@@ -0,0 +1,34 @@
+import { trpcClient } from "@renderer/trpc/client";
+import { logger } from "@utils/logger";
+
+const log = logger.scope("resolve-default-model");
+
+/**
+ * Resolve the default model for the given adapter via the preview-config
+ * tRPC query. Returns the server's `currentValue` for the `model` option, or
+ * undefined if the call fails or the option is missing.
+ *
+ * Used by inbox flows that create cloud tasks directly (Discuss, Create PR)
+ * without going through TaskInput — they need a model to pass to the saga
+ * and the user hasn't necessarily picked one yet.
+ */
+export async function resolveDefaultModel(
+ apiHost: string,
+ adapter: "claude" | "codex",
+): Promise {
+ try {
+ const options = await trpcClient.agent.getPreviewConfigOptions.query({
+ apiHost,
+ adapter,
+ });
+ const modelOption = options.find(
+ (o) => o.id === "model" || o.category === "model",
+ );
+ if (modelOption?.type === "select" && modelOption.currentValue) {
+ return modelOption.currentValue;
+ }
+ } catch (error) {
+ log.warn("Failed to resolve default model", { error, adapter });
+ }
+ return undefined;
+}