From 82da4f8a3bff714674ddc80672457499cfdc7055 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Thu, 14 May 2026 02:00:56 +0000 Subject: [PATCH 1/3] fix(tasks): sync cloud-started tasks into local workspace registry Tasks created on other surfaces (web, CLI, another machine for the same user) were already returned by the /tasks/ API but hidden in the sidebar because useSidebarData filters to tasks with a matching local workspace row. Reconcile the local workspace registry against the polled task list by creating an idempotent cloud-mode workspace row for any task missing one, then invalidate the workspaces query so the sidebar picks them up. Generated-By: PostHog Code Task-Id: bad6d25e-1cb8-4bfe-b323-7c79f6654944 --- .../src/renderer/components/MainLayout.tsx | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index d25f27967..d4668c978 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -25,17 +25,23 @@ import { useTasks } from "@features/tasks/hooks/useTasks"; import { TourOverlay } from "@features/tour/components/TourOverlay"; import { useTourStore } from "@features/tour/stores/tourStore"; import { createFirstTaskTour } from "@features/tour/tours/createFirstTaskTour"; +import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { useIntegrations } from "@hooks/useIntegrations"; import { Box, Flex } from "@radix-ui/themes"; +import { trpcClient, useTRPC } from "@renderer/trpc/client"; import { BILLING_FLAG } from "@shared/constants"; import { useCommandMenuStore } from "@stores/commandMenuStore"; import { useNavigationStore } from "@stores/navigationStore"; import { useShortcutsSheetStore } from "@stores/shortcutsSheetStore"; -import { useCallback, useEffect } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { logger } from "@utils/logger"; +import { useCallback, useEffect, useRef } from "react"; import { useTaskDeepLink } from "../hooks/useTaskDeepLink"; import { GlobalEventHandlers } from "./GlobalEventHandlers"; +const log = logger.scope("main-layout"); + export function MainLayout() { const { view, @@ -56,6 +62,10 @@ export function MainLayout() { close: closeShortcutsSheet, } = useShortcutsSheetStore(); const { data: tasks } = useTasks(); + const { data: workspaces, isFetched: workspacesFetched } = useWorkspaces(); + const trpcReact = useTRPC(); + const queryClient = useQueryClient(); + const reconcilingTaskIds = useRef>(new Set()); const billingEnabled = useFeatureFlag(BILLING_FLAG); // Space switcher data @@ -80,6 +90,37 @@ export function MainLayout() { } }, [tasks, hydrateTask]); + useEffect(() => { + if (!tasks || !workspaces || !workspacesFetched) return; + const missing = tasks.filter( + (t) => !workspaces[t.id] && !reconcilingTaskIds.current.has(t.id), + ); + if (missing.length === 0) return; + for (const t of missing) reconcilingTaskIds.current.add(t.id); + void Promise.allSettled( + missing.map((t) => + trpcClient.workspace.create.mutate({ + taskId: t.id, + mainRepoPath: "", + folderId: "", + folderPath: "", + mode: "cloud", + }), + ), + ).then((results) => { + for (const [i, r] of results.entries()) { + const id = missing[i].id; + reconcilingTaskIds.current.delete(id); + if (r.status === "rejected") { + log.warn(`Failed to reconcile workspace for task ${id}`, r.reason); + } + } + void queryClient.invalidateQueries( + trpcReact.workspace.getAll.pathFilter(), + ); + }); + }, [tasks, workspaces, workspacesFetched, queryClient, trpcReact]); + useEffect(() => { if (view.type === "task-detail" && !view.data && !view.taskId) { navigateToTaskInput(); From 0b0204f518ec87a33010aeb8770edabb58146948 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Thu, 14 May 2026 02:15:34 +0000 Subject: [PATCH 2/3] refactor: address review on cloud workspace reconciliation - Use workspaceApi.create wrapper instead of trpcClient directly - Only invalidate workspaces query when at least one mutation succeeded Generated-By: PostHog Code Task-Id: bad6d25e-1cb8-4bfe-b323-7c79f6654944 --- .../src/renderer/components/MainLayout.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index d4668c978..dceecd9f6 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -25,11 +25,14 @@ import { useTasks } from "@features/tasks/hooks/useTasks"; import { TourOverlay } from "@features/tour/components/TourOverlay"; import { useTourStore } from "@features/tour/stores/tourStore"; import { createFirstTaskTour } from "@features/tour/tours/createFirstTaskTour"; -import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; +import { + useWorkspaces, + workspaceApi, +} from "@features/workspace/hooks/useWorkspace"; import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { useIntegrations } from "@hooks/useIntegrations"; import { Box, Flex } from "@radix-ui/themes"; -import { trpcClient, useTRPC } from "@renderer/trpc/client"; +import { useTRPC } from "@renderer/trpc/client"; import { BILLING_FLAG } from "@shared/constants"; import { useCommandMenuStore } from "@stores/commandMenuStore"; import { useNavigationStore } from "@stores/navigationStore"; @@ -99,7 +102,7 @@ export function MainLayout() { for (const t of missing) reconcilingTaskIds.current.add(t.id); void Promise.allSettled( missing.map((t) => - trpcClient.workspace.create.mutate({ + workspaceApi.create({ taskId: t.id, mainRepoPath: "", folderId: "", @@ -108,16 +111,21 @@ export function MainLayout() { }), ), ).then((results) => { + let anySucceeded = false; for (const [i, r] of results.entries()) { const id = missing[i].id; reconcilingTaskIds.current.delete(id); if (r.status === "rejected") { log.warn(`Failed to reconcile workspace for task ${id}`, r.reason); + } else { + anySucceeded = true; } } - void queryClient.invalidateQueries( - trpcReact.workspace.getAll.pathFilter(), - ); + if (anySucceeded) { + void queryClient.invalidateQueries( + trpcReact.workspace.getAll.pathFilter(), + ); + } }); }, [tasks, workspaces, workspacesFetched, queryClient, trpcReact]); From 6246e73f61efe531da93c95427305f358b663410 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Thu, 14 May 2026 02:33:44 +0000 Subject: [PATCH 3/3] feat: gate cloud task reconciliation behind feature flag Wraps the new workspace reconciliation effect in MainLayout behind the posthog-code-sync-cloud-tasks flag so it only runs for users explicitly enrolled. Avoids surprising external users with unfamiliar cloud tasks that may have been created by surfaces other than the desktop app. Generated-By: PostHog Code Task-Id: bad6d25e-1cb8-4bfe-b323-7c79f6654944 --- apps/code/src/renderer/components/MainLayout.tsx | 13 +++++++++++-- apps/code/src/shared/constants.ts | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index dceecd9f6..4f712a0f4 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -33,7 +33,7 @@ import { useFeatureFlag } from "@hooks/useFeatureFlag"; import { useIntegrations } from "@hooks/useIntegrations"; import { Box, Flex } from "@radix-ui/themes"; import { useTRPC } from "@renderer/trpc/client"; -import { BILLING_FLAG } from "@shared/constants"; +import { BILLING_FLAG, SYNC_CLOUD_TASKS_FLAG } from "@shared/constants"; import { useCommandMenuStore } from "@stores/commandMenuStore"; import { useNavigationStore } from "@stores/navigationStore"; import { useShortcutsSheetStore } from "@stores/shortcutsSheetStore"; @@ -70,6 +70,7 @@ export function MainLayout() { const queryClient = useQueryClient(); const reconcilingTaskIds = useRef>(new Set()); const billingEnabled = useFeatureFlag(BILLING_FLAG); + const syncCloudTasksEnabled = useFeatureFlag(SYNC_CLOUD_TASKS_FLAG); // Space switcher data const sidebarData = useSidebarData({ activeView: view }); @@ -94,6 +95,7 @@ export function MainLayout() { }, [tasks, hydrateTask]); useEffect(() => { + if (!syncCloudTasksEnabled) return; if (!tasks || !workspaces || !workspacesFetched) return; const missing = tasks.filter( (t) => !workspaces[t.id] && !reconcilingTaskIds.current.has(t.id), @@ -127,7 +129,14 @@ export function MainLayout() { ); } }); - }, [tasks, workspaces, workspacesFetched, queryClient, trpcReact]); + }, [ + syncCloudTasksEnabled, + tasks, + workspaces, + workspacesFetched, + queryClient, + trpcReact, + ]); useEffect(() => { if (view.type === "task-detail" && !view.data && !view.taskId) { diff --git a/apps/code/src/shared/constants.ts b/apps/code/src/shared/constants.ts index 2c2a27339..eaf28c98c 100644 --- a/apps/code/src/shared/constants.ts +++ b/apps/code/src/shared/constants.ts @@ -1,5 +1,6 @@ export const BILLING_FLAG = "posthog-code-billing"; export const INBOX_GATED_DUE_TO_SCALE_FLAG = "inbox-gated-due-to-scale"; +export const SYNC_CLOUD_TASKS_FLAG = "posthog-code-sync-cloud-tasks"; export const BRANCH_PREFIX = "posthog-code/"; export const DATA_DIR = ".posthog-code"; export const WORKTREES_DIR = ".posthog-code/worktrees";