diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index dc4721d05..1b7c84953 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -171,6 +171,7 @@ interface CloudRunOptions { model?: string; reasoningLevel?: string; sandboxEnvironmentId?: string; + sandboxRuntime?: string; prAuthorshipMode?: PrAuthorshipMode; runSource?: CloudRunSource; signalReportId?: string; @@ -241,6 +242,9 @@ function buildCloudRunRequestBody( if (options?.sandboxEnvironmentId) { body.sandbox_environment_id = options.sandboxEnvironmentId; } + if (options?.sandboxRuntime) { + body.sandbox_runtime = options.sandboxRuntime; + } if (options?.prAuthorshipMode) { body.pr_authorship_mode = options.prAuthorshipMode; } diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index ef70db5c9..44c0300dd 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -122,6 +122,9 @@ export function TaskInput({ const [selectedCloudEnvId, setSelectedCloudEnvId] = useState( null, ); + const [selectedSandboxRuntime, setSelectedSandboxRuntime] = useState< + string | null + >(null); const [activeReportAssociation, setActiveReportAssociation] = useState( reportAssociation ?? null, ); @@ -489,6 +492,10 @@ export function TaskInput({ effectiveWorkspaceMode === "cloud" && selectedCloudEnvId ? selectedCloudEnvId : undefined, + sandboxRuntime: + effectiveWorkspaceMode === "cloud" && selectedSandboxRuntime + ? selectedSandboxRuntime + : undefined, signalReportId: activeReportAssociation?.reportId, }); @@ -654,6 +661,8 @@ export function TaskInput({ onChange={setWorkspaceMode} selectedCloudEnvironmentId={selectedCloudEnvId} onCloudEnvironmentChange={setSelectedCloudEnvId} + selectedSandboxRuntime={selectedSandboxRuntime} + onSandboxRuntimeChange={setSelectedSandboxRuntime} size="1" /> {workspaceMode === "worktree" && ( diff --git a/apps/code/src/renderer/features/task-detail/components/WorkspaceModeSelect.tsx b/apps/code/src/renderer/features/task-detail/components/WorkspaceModeSelect.tsx index 21d8c9380..0e66858cd 100644 --- a/apps/code/src/renderer/features/task-detail/components/WorkspaceModeSelect.tsx +++ b/apps/code/src/renderer/features/task-detail/components/WorkspaceModeSelect.tsx @@ -36,8 +36,12 @@ interface WorkspaceModeSelectProps { overrideModes?: WorkspaceMode[]; selectedCloudEnvironmentId?: string | null; onCloudEnvironmentChange?: (envId: string | null) => void; + selectedSandboxRuntime?: string | null; + onSandboxRuntimeChange?: (runtime: string | null) => void; } +const HOGLAND_RUNTIME = "posthog"; + const LOCAL_MODES: { mode: WorkspaceMode; label: string; @@ -67,9 +71,13 @@ export function WorkspaceModeSelect({ overrideModes, selectedCloudEnvironmentId, onCloudEnvironmentChange, + selectedSandboxRuntime, + onSandboxRuntimeChange, }: WorkspaceModeSelectProps) { const cloudModeEnabled = useFeatureFlag("twig-cloud-mode-toggle") || import.meta.env.DEV; + const hoglandRuntimeEnabled = + useFeatureFlag("tasks-hogland-runtime") || import.meta.env.DEV; const { environments } = useSandboxEnvironments(); const openSettings = useSettingsDialogStore((s) => s.open); @@ -97,12 +105,17 @@ export function WorkspaceModeSelect({ return environments.find((e) => e.id === selectedCloudEnvironmentId)?.name; }, [value, selectedCloudEnvironmentId, environments]); + const isHoglandActive = + value === "cloud" && selectedSandboxRuntime === HOGLAND_RUNTIME; + const triggerLabel = useMemo(() => { if (value === "cloud") { - return selectedEnvName ? `Cloud · ${selectedEnvName}` : "Cloud"; + if (selectedEnvName) return `Cloud · ${selectedEnvName}`; + if (isHoglandActive) return "Cloud · Hogland"; + return "Cloud"; } return LOCAL_MODES.find((m) => m.mode === value)?.label ?? "Worktree"; - }, [value, selectedEnvName]); + }, [value, selectedEnvName, isHoglandActive]); const triggerIcon = useMemo(() => { if (value === "cloud") return CLOUD_ICON; @@ -145,6 +158,7 @@ export function WorkspaceModeSelect({ onClick={() => { onChange(item.mode); onCloudEnvironmentChange?.(null); + onSandboxRuntimeChange?.(null); }} render={ @@ -164,25 +178,50 @@ export function WorkspaceModeSelect({ {showCloud && environments.length === 0 && ( - { - onChange("cloud"); - onCloudEnvironmentChange?.(null); - }} - render={ - - - {CLOUD_ICON} - - - Cloud - - Run in a cloud sandbox - - - - } - /> + <> + { + onChange("cloud"); + onCloudEnvironmentChange?.(null); + onSandboxRuntimeChange?.(null); + }} + render={ + + + {CLOUD_ICON} + + + Cloud + + Run in a cloud sandbox + + + + } + /> + {hoglandRuntimeEnabled && ( + { + onChange("cloud"); + onCloudEnvironmentChange?.(null); + onSandboxRuntimeChange?.(HOGLAND_RUNTIME); + }} + render={ + + + {CLOUD_ICON} + + + Cloud · Hogland + + Run on the PostHog sandbox backend + + + + } + /> + )} + )} {showCloud && environments.length > 0 && ( @@ -205,6 +244,7 @@ export function WorkspaceModeSelect({ onClick={() => { onChange("cloud"); onCloudEnvironmentChange?.(null); + onSandboxRuntimeChange?.(null); }} render={ @@ -221,12 +261,36 @@ export function WorkspaceModeSelect({ } /> + {hoglandRuntimeEnabled && ( + { + onChange("cloud"); + onCloudEnvironmentChange?.(null); + onSandboxRuntimeChange?.(HOGLAND_RUNTIME); + }} + render={ + + + {CLOUD_ICON} + + + Hogland + + PostHog sandbox backend + + + + } + /> + )} + {environments.map((env) => ( { onChange("cloud"); onCloudEnvironmentChange?.(env.id); + onSandboxRuntimeChange?.(null); }} render={ diff --git a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts index 3d4fe4d7c..b66fdbf36 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts @@ -44,6 +44,7 @@ interface UseTaskCreationOptions { reasoningLevel?: string; environmentId?: string | null; sandboxEnvironmentId?: string; + sandboxRuntime?: string; signalReportId?: string; onTaskCreated?: (task: Task) => void; } @@ -69,6 +70,7 @@ function prepareTaskInput( reasoningLevel?: string; environmentId?: string | null; sandboxEnvironmentId?: string; + sandboxRuntime?: string; signalReportId?: string; }, ): TaskCreationInput { @@ -98,6 +100,7 @@ function prepareTaskInput( reasoningLevel: options.reasoningLevel, environmentId: options.environmentId ?? undefined, sandboxEnvironmentId: options.sandboxEnvironmentId, + sandboxRuntime: options.sandboxRuntime, cloudPrAuthorshipMode: options.signalReportId && options.workspaceMode === "cloud" ? "user" @@ -185,6 +188,7 @@ export function useTaskCreation({ reasoningLevel, environmentId, sandboxEnvironmentId, + sandboxRuntime, signalReportId, onTaskCreated, }: UseTaskCreationOptions): UseTaskCreationReturn { @@ -258,6 +262,7 @@ export function useTaskCreation({ reasoningLevel, environmentId, sandboxEnvironmentId, + sandboxRuntime, signalReportId, }); @@ -332,6 +337,7 @@ export function useTaskCreation({ reasoningLevel, environmentId, sandboxEnvironmentId, + sandboxRuntime, signalReportId, clearTaskInputReportAssociation, invalidateTasks, diff --git a/apps/code/src/renderer/sagas/task/task-creation.ts b/apps/code/src/renderer/sagas/task/task-creation.ts index 582c0b94a..f08b9e7b2 100644 --- a/apps/code/src/renderer/sagas/task/task-creation.ts +++ b/apps/code/src/renderer/sagas/task/task-creation.ts @@ -57,6 +57,7 @@ export interface TaskCreationInput { reasoningLevel?: string; environmentId?: string; sandboxEnvironmentId?: string; + sandboxRuntime?: string; cloudPrAuthorshipMode?: PrAuthorshipMode; cloudRunSource?: CloudRunSource; signalReportId?: string; @@ -248,6 +249,7 @@ export class TaskCreationSaga extends Saga< model: input.model, reasoningLevel: input.reasoningLevel, sandboxEnvironmentId: input.sandboxEnvironmentId, + sandboxRuntime: input.sandboxRuntime, prAuthorshipMode, runSource: input.cloudRunSource ?? "manual", signalReportId: input.signalReportId,