diff --git a/apps/mobile/src/app/settings/index.tsx b/apps/mobile/src/app/settings/index.tsx index fa11f065d9..d5784feddb 100644 --- a/apps/mobile/src/app/settings/index.tsx +++ b/apps/mobile/src/app/settings/index.tsx @@ -193,6 +193,12 @@ export default function SettingsScreen() { const setDefaultReasoningEffort = usePreferencesStore( (s) => s.setDefaultReasoningEffort, ); + const autoPublishCloudRuns = usePreferencesStore( + (s) => s.autoPublishCloudRuns, + ); + const setAutoPublishCloudRuns = usePreferencesStore( + (s) => s.setAutoPublishCloudRuns, + ); const defaultMessagingMode = useMessagingModeStore((s) => s.defaultMode); const setDefaultMessagingMode = useMessagingModeStore( (s) => s.setDefaultMode, @@ -409,7 +415,6 @@ export default function SettingsScreen() { label="Messaging mode" description="What happens when you send while a turn is running" onPress={() => setMessagingModeSheetOpen(true)} - showDivider={false} rightSlot={ <> @@ -419,6 +424,17 @@ export default function SettingsScreen() { } /> + + } + /> {/* Integrations */} diff --git a/apps/mobile/src/app/task/index.tsx b/apps/mobile/src/app/task/index.tsx index ffc5e241de..882d1b92c0 100644 --- a/apps/mobile/src/app/task/index.tsx +++ b/apps/mobile/src/app/task/index.tsx @@ -351,6 +351,7 @@ export default function NewTaskScreen() { model, reasoningEffort: supportsReasoning ? reasoning : undefined, initialPermissionMode: mode, + autoPublish: usePreferencesStore.getState().autoPublishCloudRuns, ...(signalReport ? { runSource: "signal_report" as const, diff --git a/apps/mobile/src/features/preferences/stores/preferencesStore.ts b/apps/mobile/src/features/preferences/stores/preferencesStore.ts index cc8b5475fc..bdf8a3b64f 100644 --- a/apps/mobile/src/features/preferences/stores/preferencesStore.ts +++ b/apps/mobile/src/features/preferences/stores/preferencesStore.ts @@ -80,6 +80,9 @@ interface PreferencesState { * `defaultReasoningEffort === "last_used"` can pre-fill it next time. */ lastUsedReasoningEffort: string; setLastUsedReasoningEffort: (effort: string) => void; + + autoPublishCloudRuns: boolean; + setAutoPublishCloudRuns: (enabled: boolean) => void; } export const usePreferencesStore = create()( @@ -120,6 +123,10 @@ export const usePreferencesStore = create()( lastUsedReasoningEffort: "high", setLastUsedReasoningEffort: (effort) => set({ lastUsedReasoningEffort: effort }), + + autoPublishCloudRuns: true, + setAutoPublishCloudRuns: (enabled) => + set({ autoPublishCloudRuns: enabled }), }), { name: "posthog-preferences", @@ -136,6 +143,7 @@ export const usePreferencesStore = create()( lastNewTaskMode: state.lastNewTaskMode, defaultReasoningEffort: state.defaultReasoningEffort, lastUsedReasoningEffort: state.lastUsedReasoningEffort, + autoPublishCloudRuns: state.autoPublishCloudRuns, }), }, ), diff --git a/apps/mobile/src/features/tasks/api.test.ts b/apps/mobile/src/features/tasks/api.test.ts new file mode 100644 index 0000000000..e56acbb760 --- /dev/null +++ b/apps/mobile/src/features/tasks/api.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockFetch } = vi.hoisted(() => ({ + mockFetch: vi.fn(), +})); + +vi.mock("expo/fetch", () => ({ + fetch: mockFetch, +})); + +vi.mock("@/lib/api", () => ({ + getBaseUrl: () => "https://app.posthog.test", + getProjectId: () => 42, + getAccessToken: () => "token", + createTimeoutSignal: () => undefined, + authedFetch: (url: string, init?: RequestInit) => + mockFetch(url, { + ...init, + headers: { + Authorization: "Bearer token", + "Content-Type": "application/json", + ...((init?.headers as Record | undefined) ?? {}), + }, + }), +})); + +import { runTaskInCloud } from "./api"; + +function bodyOf(call: unknown): Record { + const [, init] = call as [string, RequestInit]; + return JSON.parse(init.body as string); +} + +describe("runTaskInCloud", () => { + beforeEach(() => { + mockFetch.mockReset(); + mockFetch.mockResolvedValue( + new Response(JSON.stringify({ id: "task-1" }), { status: 200 }), + ); + }); + + it.each([true, false])( + "forwards auto_publish=%s to the payload", + async (flag) => { + await runTaskInCloud("task-1", { autoPublish: flag }); + + expect(bodyOf(mockFetch.mock.calls[0])).toMatchObject({ + auto_publish: flag, + }); + }, + ); + + it("omits auto_publish when not provided", async () => { + await runTaskInCloud("task-1", { model: "claude-opus-4-8" }); + + expect(bodyOf(mockFetch.mock.calls[0])).not.toHaveProperty("auto_publish"); + }); + + it("sends no body for the plain initial run", async () => { + await runTaskInCloud("task-1"); + + const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]; + expect(init.body).toBeUndefined(); + }); +}); diff --git a/apps/mobile/src/features/tasks/api.ts b/apps/mobile/src/features/tasks/api.ts index f237666f02..68fae4895f 100644 --- a/apps/mobile/src/features/tasks/api.ts +++ b/apps/mobile/src/features/tasks/api.ts @@ -433,6 +433,9 @@ export interface RunTaskInCloudOptions { runSource?: "manual" | "signal_report"; /** Signal report ID when run_source is "signal_report". */ signalReportId?: string; + /** When true, the cloud run pushes its changes and opens a draft PR on + * completion without waiting for an explicit ask. */ + autoPublish?: boolean; } export async function runTaskInCloud( @@ -456,7 +459,8 @@ export async function runTaskInCloud( options.reasoningEffort !== undefined || options.initialPermissionMode !== undefined || options.runSource !== undefined || - options.signalReportId !== undefined); + options.signalReportId !== undefined || + options.autoPublish !== undefined); let body: string | undefined; if (hasOptions) { @@ -483,6 +487,9 @@ export async function runTaskInCloud( if (options?.runSource) payload.run_source = options.runSource; if (options?.signalReportId) payload.signal_report_id = options.signalReportId; + if (options?.autoPublish !== undefined) { + payload.auto_publish = options.autoPublish; + } body = JSON.stringify(payload); }