From 478c0e4f2891d8809f7b1be9a8b6238f758a7045 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 18:42:36 +0200 Subject: [PATCH 1/8] Restore the last app location on startup Generated-By: PostHog Code Task-Id: c6ae4672-c246-4078-857d-b00e81aaaaaa --- .../ui/src/router/startupLocation.test.ts | 56 +++++++++++++ packages/ui/src/router/startupLocation.ts | 81 +++++++++++++++++++ packages/ui/src/shell/App.tsx | 47 ++++++++++- packages/ui/src/shell/rendererStorage.ts | 12 +++ 4 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/router/startupLocation.test.ts create mode 100644 packages/ui/src/router/startupLocation.ts diff --git a/packages/ui/src/router/startupLocation.test.ts b/packages/ui/src/router/startupLocation.test.ts new file mode 100644 index 0000000000..4270d8f921 --- /dev/null +++ b/packages/ui/src/router/startupLocation.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it, vi } from "vitest"; +import { + canRestoreLocation, + isRestorableLocation, + personalNewTaskLocation, +} from "./startupLocation"; + +describe("startupLocation", () => { + it("restores stable screens but not landing or pending screens", () => { + expect(isRestorableLocation("/code/tasks/task-1")).toBe(true); + expect(isRestorableLocation("/settings/general")).toBe(true); + expect(isRestorableLocation("/code")).toBe(false); + expect(isRestorableLocation("/code/tasks/pending/create-1")).toBe(false); + }); + + it("opens a new task in an existing me space", async () => { + const client = { + getDesktopFileSystemChannels: vi + .fn() + .mockResolvedValue([{ id: "me-id", path: "me", type: "folder" }]), + createDesktopFileSystemChannel: vi.fn(), + }; + + await expect(personalNewTaskLocation(client as never)).resolves.toBe( + "/website/me-id/new", + ); + expect(client.createDesktopFileSystemChannel).not.toHaveBeenCalled(); + }); + + it("creates the me space on first use", async () => { + const client = { + getDesktopFileSystemChannels: vi.fn().mockResolvedValue([]), + createDesktopFileSystemChannel: vi + .fn() + .mockResolvedValue({ id: "new-me-id", path: "me", type: "folder" }), + }; + + await expect(personalNewTaskLocation(client as never)).resolves.toBe( + "/website/new-me-id/new", + ); + }); + + it("rejects a deleted task or space", async () => { + const client = { + getTask: vi.fn().mockRejectedValue(new Error("Not found")), + getDesktopFileSystemChannels: vi.fn().mockResolvedValue([]), + }; + + await expect( + canRestoreLocation(client as never, "/code/tasks/deleted"), + ).resolves.toBe(false); + await expect( + canRestoreLocation(client as never, "/website/deleted/context"), + ).resolves.toBe(false); + }); +}); diff --git a/packages/ui/src/router/startupLocation.ts b/packages/ui/src/router/startupLocation.ts new file mode 100644 index 0000000000..ac4377244f --- /dev/null +++ b/packages/ui/src/router/startupLocation.ts @@ -0,0 +1,81 @@ +import type { PostHogAPIClient } from "@posthog/api-client/posthog-client"; +import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersonalChannel"; +import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels"; +import { + readRendererState, + writeRendererState, +} from "@posthog/ui/shell/rendererStorage"; + +const KEY_PREFIX = "startup-location:"; + +function key(identity: string): string { + return `${KEY_PREFIX}${identity}`; +} + +export async function readStartupLocation( + identity: string, +): Promise { + return await readRendererState(key(identity)); +} + +export async function writeStartupLocation( + identity: string, + href: string, +): Promise { + await writeRendererState(key(identity), href); +} + +export function isRestorableLocation(href: string): boolean { + return ( + href.startsWith("/") && + href !== "/" && + href !== "/code" && + href !== "/code/" && + href !== "/website/new" && + !href.startsWith("/code/tasks/pending/") + ); +} + +export async function canRestoreLocation( + client: PostHogAPIClient, + href: string, +): Promise { + const taskId = href.match(/^\/code\/tasks\/([^/?#]+)/)?.[1]; + if (taskId) { + try { + await client.getTask(decodeURIComponent(taskId)); + return true; + } catch { + return false; + } + } + + const channelId = href.match(/^\/website\/([^/?#]+)/)?.[1]; + if (channelId && channelId !== "new") { + try { + return (await client.getDesktopFileSystemChannels()).some( + (channel) => channel.id === decodeURIComponent(channelId), + ); + } catch { + return false; + } + } + return true; +} + +export async function personalNewTaskLocation( + client: PostHogAPIClient, +): Promise { + const toChannel = (channel: { id: string; path: string }): Channel => ({ + id: channel.id, + name: channel.path.replace(/^\/+/, ""), + path: channel.path, + }); + const channels = (await client.getDesktopFileSystemChannels()) + .filter((channel) => channel.type === "folder") + .map(toChannel); + const personal = await ensurePersonalChannel(channels, async (name) => + toChannel(await client.createDesktopFileSystemChannel(name)), + ); + return `/website/${personal.id}/new`; +} diff --git a/packages/ui/src/shell/App.tsx b/packages/ui/src/shell/App.tsx index 4963ab3970..6861c8819e 100644 --- a/packages/ui/src/shell/App.tsx +++ b/packages/ui/src/shell/App.tsx @@ -21,6 +21,13 @@ import { SettingsDialog } from "@posthog/ui/features/settings/SettingsDialog"; import { UpdateBanner } from "@posthog/ui/features/sidebar/components/UpdateBanner"; import { PendingPromptRecovery } from "@posthog/ui/features/task-detail/components/PendingPromptRecovery"; import { router } from "@posthog/ui/router/router"; +import { + canRestoreLocation, + isRestorableLocation, + personalNewTaskLocation, + readStartupLocation, + writeStartupLocation, +} from "@posthog/ui/router/startupLocation"; import { AppLoadingScreen } from "@posthog/ui/shell/AppLoadingScreen"; import { track } from "@posthog/ui/shell/analytics"; import { ErrorBoundary } from "@posthog/ui/shell/ErrorBoundary"; @@ -88,6 +95,12 @@ function App({ devToolbar }: AppProps) { !isCheckingAccess && !needsInviteCode && !needsAiApproval; + const startupIdentity = + authState.status === "authenticated" && + authState.cloudRegion && + authState.currentProjectId != null + ? `${authState.cloudRegion}:${authState.currentProjectId}` + : null; // Run the initial route's loaders before the router ever mounts, so the boot // loading screen holds until the route is ready. The router turns loader @@ -96,14 +109,26 @@ function App({ devToolbar }: AppProps) { // re-entry loads fresh. const [initialRouteLoaded, setInitialRouteLoaded] = useState(false); useEffect(() => { - if (!readyForMainApp) { + if (!readyForMainApp || !startupIdentity || !authenticatedClient) { setInitialRouteLoaded(false); return; } if (initialRouteLoaded) return; let cancelled = false; - void router - .load() + void readStartupLocation(startupIdentity) + .then(async (saved) => { + const canRestore = + saved !== null && + isRestorableLocation(saved) && + (await canRestoreLocation(authenticatedClient, saved)); + const href = + canRestore && saved + ? saved + : await personalNewTaskLocation(authenticatedClient); + router.history.replace(href); + await writeStartupLocation(startupIdentity, href); + await router.load(); + }) .catch(() => undefined) .finally(() => { if (!cancelled) setInitialRouteLoaded(true); @@ -111,7 +136,21 @@ function App({ devToolbar }: AppProps) { return () => { cancelled = true; }; - }, [readyForMainApp, initialRouteLoaded]); + }, [ + readyForMainApp, + initialRouteLoaded, + startupIdentity, + authenticatedClient, + ]); + + useEffect(() => { + if (!initialRouteLoaded || !startupIdentity) return; + return router.history.subscribe(({ location }) => { + if (isRestorableLocation(location.href)) { + void writeStartupLocation(startupIdentity, location.href); + } + }); + }, [initialRouteLoaded, startupIdentity]); const mainRef = useRef(null); // Mirrors the "main" branch of renderContent() below; keep the two in sync. diff --git a/packages/ui/src/shell/rendererStorage.ts b/packages/ui/src/shell/rendererStorage.ts index 7bc95cf11d..5e56a53fcd 100644 --- a/packages/ui/src/shell/rendererStorage.ts +++ b/packages/ui/src/shell/rendererStorage.ts @@ -118,6 +118,18 @@ export function registerRendererStateStorage( hostStorageReady.resolve(storage); } +/** Small imperative seam for persisted state that must be read before React mounts. */ +export async function readRendererState(key: string): Promise { + return await deferredHostStorage.getItem(key); +} + +export async function writeRendererState( + key: string, + value: string, +): Promise { + await deferredHostStorage.setItem(key, value); +} + const deferredHostStorage: StateStorage = { getItem: async (key) => { // A coalesced write that has not flushed yet is newer than the backend From 2038b65677e6bf5154aa59d9eab6e52aeeae60ce Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 18:49:26 +0200 Subject: [PATCH 2/8] Tighten startup location boundaries Generated-By: PostHog Code Task-Id: c6ae4672-c246-4078-857d-b00e81aaaaaa --- .../ui/src/router/startupLocation.test.ts | 8 +-- packages/ui/src/router/startupLocation.ts | 62 +++++++++++-------- packages/ui/src/shell/App.tsx | 25 ++------ packages/ui/src/shell/rendererStorage.ts | 16 +---- 4 files changed, 49 insertions(+), 62 deletions(-) diff --git a/packages/ui/src/router/startupLocation.test.ts b/packages/ui/src/router/startupLocation.test.ts index 4270d8f921..94673cd87d 100644 --- a/packages/ui/src/router/startupLocation.test.ts +++ b/packages/ui/src/router/startupLocation.test.ts @@ -21,7 +21,7 @@ describe("startupLocation", () => { createDesktopFileSystemChannel: vi.fn(), }; - await expect(personalNewTaskLocation(client as never)).resolves.toBe( + await expect(personalNewTaskLocation(client)).resolves.toBe( "/website/me-id/new", ); expect(client.createDesktopFileSystemChannel).not.toHaveBeenCalled(); @@ -35,7 +35,7 @@ describe("startupLocation", () => { .mockResolvedValue({ id: "new-me-id", path: "me", type: "folder" }), }; - await expect(personalNewTaskLocation(client as never)).resolves.toBe( + await expect(personalNewTaskLocation(client)).resolves.toBe( "/website/new-me-id/new", ); }); @@ -47,10 +47,10 @@ describe("startupLocation", () => { }; await expect( - canRestoreLocation(client as never, "/code/tasks/deleted"), + canRestoreLocation(client, "/code/tasks/deleted"), ).resolves.toBe(false); await expect( - canRestoreLocation(client as never, "/website/deleted/context"), + canRestoreLocation(client, "/website/deleted/context"), ).resolves.toBe(false); }); }); diff --git a/packages/ui/src/router/startupLocation.ts b/packages/ui/src/router/startupLocation.ts index ac4377244f..b33d68a7d8 100644 --- a/packages/ui/src/router/startupLocation.ts +++ b/packages/ui/src/router/startupLocation.ts @@ -1,28 +1,45 @@ import type { PostHogAPIClient } from "@posthog/api-client/posthog-client"; import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersonalChannel"; import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels"; -import { - readRendererState, - writeRendererState, -} from "@posthog/ui/shell/rendererStorage"; +import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; -const KEY_PREFIX = "startup-location:"; +type ChannelClient = Pick< + PostHogAPIClient, + "createDesktopFileSystemChannel" | "getDesktopFileSystemChannels" +>; +type RestoreClient = Pick< + PostHogAPIClient, + "getDesktopFileSystemChannels" | "getTask" +>; +type StartupClient = ChannelClient & RestoreClient; -function key(identity: string): string { - return `${KEY_PREFIX}${identity}`; -} +const storageKey = (identity: string): string => `startup-location:${identity}`; +const channelFromPath = ({ + id, + path, +}: { + id: string; + path: string; +}): Channel => ({ id, name: path.replace(/^\/+/, ""), path }); -export async function readStartupLocation( +export async function resolveStartupLocation( identity: string, -): Promise { - return await readRendererState(key(identity)); + client: StartupClient, +): Promise { + const saved = await rendererStateStorage.getItem(storageKey(identity)); + if ( + saved && + isRestorableLocation(saved) && + (await canRestoreLocation(client, saved)) + ) { + return saved; + } + return await personalNewTaskLocation(client); } -export async function writeStartupLocation( - identity: string, - href: string, -): Promise { - await writeRendererState(key(identity), href); +export function rememberStartupLocation(identity: string, href: string): void { + if (!isRestorableLocation(href)) return; + void rendererStateStorage.setItem(storageKey(identity), href); } export function isRestorableLocation(href: string): boolean { @@ -37,7 +54,7 @@ export function isRestorableLocation(href: string): boolean { } export async function canRestoreLocation( - client: PostHogAPIClient, + client: RestoreClient, href: string, ): Promise { const taskId = href.match(/^\/code\/tasks\/([^/?#]+)/)?.[1]; @@ -64,18 +81,13 @@ export async function canRestoreLocation( } export async function personalNewTaskLocation( - client: PostHogAPIClient, + client: ChannelClient, ): Promise { - const toChannel = (channel: { id: string; path: string }): Channel => ({ - id: channel.id, - name: channel.path.replace(/^\/+/, ""), - path: channel.path, - }); const channels = (await client.getDesktopFileSystemChannels()) .filter((channel) => channel.type === "folder") - .map(toChannel); + .map(channelFromPath); const personal = await ensurePersonalChannel(channels, async (name) => - toChannel(await client.createDesktopFileSystemChannel(name)), + channelFromPath(await client.createDesktopFileSystemChannel(name)), ); return `/website/${personal.id}/new`; } diff --git a/packages/ui/src/shell/App.tsx b/packages/ui/src/shell/App.tsx index 6861c8819e..4f2c0585ac 100644 --- a/packages/ui/src/shell/App.tsx +++ b/packages/ui/src/shell/App.tsx @@ -22,11 +22,8 @@ import { UpdateBanner } from "@posthog/ui/features/sidebar/components/UpdateBann import { PendingPromptRecovery } from "@posthog/ui/features/task-detail/components/PendingPromptRecovery"; import { router } from "@posthog/ui/router/router"; import { - canRestoreLocation, - isRestorableLocation, - personalNewTaskLocation, - readStartupLocation, - writeStartupLocation, + rememberStartupLocation, + resolveStartupLocation, } from "@posthog/ui/router/startupLocation"; import { AppLoadingScreen } from "@posthog/ui/shell/AppLoadingScreen"; import { track } from "@posthog/ui/shell/analytics"; @@ -115,18 +112,10 @@ function App({ devToolbar }: AppProps) { } if (initialRouteLoaded) return; let cancelled = false; - void readStartupLocation(startupIdentity) - .then(async (saved) => { - const canRestore = - saved !== null && - isRestorableLocation(saved) && - (await canRestoreLocation(authenticatedClient, saved)); - const href = - canRestore && saved - ? saved - : await personalNewTaskLocation(authenticatedClient); + void resolveStartupLocation(startupIdentity, authenticatedClient) + .then(async (href) => { router.history.replace(href); - await writeStartupLocation(startupIdentity, href); + rememberStartupLocation(startupIdentity, href); await router.load(); }) .catch(() => undefined) @@ -146,9 +135,7 @@ function App({ devToolbar }: AppProps) { useEffect(() => { if (!initialRouteLoaded || !startupIdentity) return; return router.history.subscribe(({ location }) => { - if (isRestorableLocation(location.href)) { - void writeStartupLocation(startupIdentity, location.href); - } + rememberStartupLocation(startupIdentity, location.href); }); }, [initialRouteLoaded, startupIdentity]); diff --git a/packages/ui/src/shell/rendererStorage.ts b/packages/ui/src/shell/rendererStorage.ts index 5e56a53fcd..9d17b3c97b 100644 --- a/packages/ui/src/shell/rendererStorage.ts +++ b/packages/ui/src/shell/rendererStorage.ts @@ -118,19 +118,7 @@ export function registerRendererStateStorage( hostStorageReady.resolve(storage); } -/** Small imperative seam for persisted state that must be read before React mounts. */ -export async function readRendererState(key: string): Promise { - return await deferredHostStorage.getItem(key); -} - -export async function writeRendererState( - key: string, - value: string, -): Promise { - await deferredHostStorage.setItem(key, value); -} - -const deferredHostStorage: StateStorage = { +export const rendererStateStorage: StateStorage = { getItem: async (key) => { // A coalesced write that has not flushed yet is newer than the backend // copy; land it first so the read never observes older state. A queued @@ -182,4 +170,4 @@ const deferredHostStorage: StateStorage = { }, }; -export const electronStorage = createJSONStorage(() => deferredHostStorage); +export const electronStorage = createJSONStorage(() => rendererStateStorage); From 969b86acf72992ff1453a0c4c4748b440559cdf4 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 19:28:58 +0200 Subject: [PATCH 3/8] Simplify startup location restoration Generated-By: PostHog Code Task-Id: c6ae4672-c246-4078-857d-b00e81aaaaaa --- .../src/features/canvas/hooks/useChannels.ts | 2 +- .../ui/src/router/startupLocation.test.ts | 38 +++++----- packages/ui/src/router/startupLocation.ts | 69 ++----------------- packages/ui/src/shell/App.tsx | 12 ++-- 4 files changed, 26 insertions(+), 95 deletions(-) diff --git a/packages/ui/src/features/canvas/hooks/useChannels.ts b/packages/ui/src/features/canvas/hooks/useChannels.ts index ba2e78945a..ca3f05b7de 100644 --- a/packages/ui/src/features/canvas/hooks/useChannels.ts +++ b/packages/ui/src/features/canvas/hooks/useChannels.ts @@ -26,7 +26,7 @@ export interface Channel { homeCanvasId?: string; } -function toChannel(fs: Schemas.FileSystem): Channel { +export function toChannel(fs: Schemas.FileSystem): Channel { // The generated OpenAPI type declares `meta` as null, but the API returns our // free-form blob at runtime; read homeCanvasId past the type. const meta = fs.meta as { homeCanvasId?: string } | null | undefined; diff --git a/packages/ui/src/router/startupLocation.test.ts b/packages/ui/src/router/startupLocation.test.ts index 94673cd87d..4a4def0613 100644 --- a/packages/ui/src/router/startupLocation.test.ts +++ b/packages/ui/src/router/startupLocation.test.ts @@ -1,16 +1,24 @@ -import { describe, expect, it, vi } from "vitest"; +import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { - canRestoreLocation, - isRestorableLocation, personalNewTaskLocation, + resolveStartupLocation, } from "./startupLocation"; describe("startupLocation", () => { - it("restores stable screens but not landing or pending screens", () => { - expect(isRestorableLocation("/code/tasks/task-1")).toBe(true); - expect(isRestorableLocation("/settings/general")).toBe(true); - expect(isRestorableLocation("/code")).toBe(false); - expect(isRestorableLocation("/code/tasks/pending/create-1")).toBe(false); + afterEach(() => vi.restoreAllMocks()); + + it("restores the exact last location", async () => { + vi.spyOn(rendererStateStorage, "getItem").mockResolvedValue("/code"); + const client = { + getDesktopFileSystemChannels: vi.fn(), + createDesktopFileSystemChannel: vi.fn(), + }; + + await expect(resolveStartupLocation("project", client)).resolves.toBe( + "/code", + ); + expect(client.getDesktopFileSystemChannels).not.toHaveBeenCalled(); }); it("opens a new task in an existing me space", async () => { @@ -39,18 +47,4 @@ describe("startupLocation", () => { "/website/new-me-id/new", ); }); - - it("rejects a deleted task or space", async () => { - const client = { - getTask: vi.fn().mockRejectedValue(new Error("Not found")), - getDesktopFileSystemChannels: vi.fn().mockResolvedValue([]), - }; - - await expect( - canRestoreLocation(client, "/code/tasks/deleted"), - ).resolves.toBe(false); - await expect( - canRestoreLocation(client, "/website/deleted/context"), - ).resolves.toBe(false); - }); }); diff --git a/packages/ui/src/router/startupLocation.ts b/packages/ui/src/router/startupLocation.ts index b33d68a7d8..c33ec091f7 100644 --- a/packages/ui/src/router/startupLocation.ts +++ b/packages/ui/src/router/startupLocation.ts @@ -1,93 +1,34 @@ import type { PostHogAPIClient } from "@posthog/api-client/posthog-client"; import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersonalChannel"; -import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels"; +import { toChannel } from "@posthog/ui/features/canvas/hooks/useChannels"; import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; type ChannelClient = Pick< PostHogAPIClient, "createDesktopFileSystemChannel" | "getDesktopFileSystemChannels" >; -type RestoreClient = Pick< - PostHogAPIClient, - "getDesktopFileSystemChannels" | "getTask" ->; -type StartupClient = ChannelClient & RestoreClient; - const storageKey = (identity: string): string => `startup-location:${identity}`; -const channelFromPath = ({ - id, - path, -}: { - id: string; - path: string; -}): Channel => ({ id, name: path.replace(/^\/+/, ""), path }); export async function resolveStartupLocation( identity: string, - client: StartupClient, + client: ChannelClient, ): Promise { const saved = await rendererStateStorage.getItem(storageKey(identity)); - if ( - saved && - isRestorableLocation(saved) && - (await canRestoreLocation(client, saved)) - ) { - return saved; - } - return await personalNewTaskLocation(client); + return saved ?? (await personalNewTaskLocation(client)); } export function rememberStartupLocation(identity: string, href: string): void { - if (!isRestorableLocation(href)) return; void rendererStateStorage.setItem(storageKey(identity), href); } -export function isRestorableLocation(href: string): boolean { - return ( - href.startsWith("/") && - href !== "/" && - href !== "/code" && - href !== "/code/" && - href !== "/website/new" && - !href.startsWith("/code/tasks/pending/") - ); -} - -export async function canRestoreLocation( - client: RestoreClient, - href: string, -): Promise { - const taskId = href.match(/^\/code\/tasks\/([^/?#]+)/)?.[1]; - if (taskId) { - try { - await client.getTask(decodeURIComponent(taskId)); - return true; - } catch { - return false; - } - } - - const channelId = href.match(/^\/website\/([^/?#]+)/)?.[1]; - if (channelId && channelId !== "new") { - try { - return (await client.getDesktopFileSystemChannels()).some( - (channel) => channel.id === decodeURIComponent(channelId), - ); - } catch { - return false; - } - } - return true; -} - export async function personalNewTaskLocation( client: ChannelClient, ): Promise { const channels = (await client.getDesktopFileSystemChannels()) .filter((channel) => channel.type === "folder") - .map(channelFromPath); + .map(toChannel); const personal = await ensurePersonalChannel(channels, async (name) => - channelFromPath(await client.createDesktopFileSystemChannel(name)), + toChannel(await client.createDesktopFileSystemChannel(name)), ); return `/website/${personal.id}/new`; } diff --git a/packages/ui/src/shell/App.tsx b/packages/ui/src/shell/App.tsx index 4f2c0585ac..5ce639eac9 100644 --- a/packages/ui/src/shell/App.tsx +++ b/packages/ui/src/shell/App.tsx @@ -1,3 +1,4 @@ +import { getAuthIdentity } from "@posthog/core/auth/authIdentity"; import { ToastProvider } from "@posthog/quill"; import { EXTERNAL_LINKS, isNotAuthenticatedError } from "@posthog/shared"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; @@ -92,12 +93,7 @@ function App({ devToolbar }: AppProps) { !isCheckingAccess && !needsInviteCode && !needsAiApproval; - const startupIdentity = - authState.status === "authenticated" && - authState.cloudRegion && - authState.currentProjectId != null - ? `${authState.cloudRegion}:${authState.currentProjectId}` - : null; + const startupIdentity = getAuthIdentity(authState); // Run the initial route's loaders before the router ever mounts, so the boot // loading screen holds until the route is ready. The router turns loader @@ -106,11 +102,11 @@ function App({ devToolbar }: AppProps) { // re-entry loads fresh. const [initialRouteLoaded, setInitialRouteLoaded] = useState(false); useEffect(() => { - if (!readyForMainApp || !startupIdentity || !authenticatedClient) { + if (!readyForMainApp) { setInitialRouteLoaded(false); return; } - if (initialRouteLoaded) return; + if (initialRouteLoaded || !startupIdentity || !authenticatedClient) return; let cancelled = false; void resolveStartupLocation(startupIdentity, authenticatedClient) .then(async (href) => { From 5939e4e94a92c5b6c32a1962b9453e6aa763ed14 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 19:38:20 +0200 Subject: [PATCH 4/8] Clarify initial route loading Generated-By: PostHog Code Task-Id: c6ae4672-c246-4078-857d-b00e81aaaaaa --- packages/ui/src/shell/App.tsx | 41 +++++++++++-------- .../{router => shell}/startupLocation.test.ts | 2 +- .../src/{router => shell}/startupLocation.ts | 6 +-- 3 files changed, 29 insertions(+), 20 deletions(-) rename packages/ui/src/{router => shell}/startupLocation.test.ts (97%) rename packages/ui/src/{router => shell}/startupLocation.ts (92%) diff --git a/packages/ui/src/shell/App.tsx b/packages/ui/src/shell/App.tsx index 5ce639eac9..d74f0a03d9 100644 --- a/packages/ui/src/shell/App.tsx +++ b/packages/ui/src/shell/App.tsx @@ -22,14 +22,15 @@ import { SettingsDialog } from "@posthog/ui/features/settings/SettingsDialog"; import { UpdateBanner } from "@posthog/ui/features/sidebar/components/UpdateBanner"; import { PendingPromptRecovery } from "@posthog/ui/features/task-detail/components/PendingPromptRecovery"; import { router } from "@posthog/ui/router/router"; -import { - rememberStartupLocation, - resolveStartupLocation, -} from "@posthog/ui/router/startupLocation"; import { AppLoadingScreen } from "@posthog/ui/shell/AppLoadingScreen"; import { track } from "@posthog/ui/shell/analytics"; import { ErrorBoundary } from "@posthog/ui/shell/ErrorBoundary"; +import { logger } from "@posthog/ui/shell/logger"; import { openExternalUrl } from "@posthog/ui/shell/openExternal"; +import { + rememberStartupLocation, + resolveStartupLocation, +} from "@posthog/ui/shell/startupLocation"; import { useAppVisibilityWatchdog } from "@posthog/ui/shell/useAppVisibilityWatchdog"; import { RouterProvider } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion"; @@ -40,6 +41,8 @@ interface AppProps { devToolbar?: ReactNode; } +const log = logger.scope("app"); + function App({ devToolbar }: AppProps) { const { isBootstrapped } = useAuthSession(); const authState = useAuthStateValue((state) => state); @@ -95,29 +98,35 @@ function App({ devToolbar }: AppProps) { !needsAiApproval; const startupIdentity = getAuthIdentity(authState); - // Run the initial route's loaders before the router ever mounts, so the boot - // loading screen holds until the route is ready. The router turns loader - // errors into route error UI itself; the catch is only unhandled-rejection - // hygiene. Resets when the user leaves the main app (logout, gates) so - // re-entry loads fresh. + // Resolve and load the initial route before mounting the router. Reset when + // the user leaves the main app so a later re-entry starts fresh. const [initialRouteLoaded, setInitialRouteLoaded] = useState(false); useEffect(() => { if (!readyForMainApp) { setInitialRouteLoaded(false); return; } - if (initialRouteLoaded || !startupIdentity || !authenticatedClient) return; + if (initialRouteLoaded) return; + if (!startupIdentity || !authenticatedClient) return; + let cancelled = false; - void resolveStartupLocation(startupIdentity, authenticatedClient) - .then(async (href) => { + const loadInitialRoute = async (): Promise => { + try { + const href = await resolveStartupLocation( + startupIdentity, + authenticatedClient, + ); router.history.replace(href); rememberStartupLocation(startupIdentity, href); await router.load(); - }) - .catch(() => undefined) - .finally(() => { + } catch (error) { + log.error("Failed to load initial route", { error }); + } finally { if (!cancelled) setInitialRouteLoaded(true); - }); + } + }; + void loadInitialRoute(); + return () => { cancelled = true; }; diff --git a/packages/ui/src/router/startupLocation.test.ts b/packages/ui/src/shell/startupLocation.test.ts similarity index 97% rename from packages/ui/src/router/startupLocation.test.ts rename to packages/ui/src/shell/startupLocation.test.ts index 4a4def0613..267ef92695 100644 --- a/packages/ui/src/router/startupLocation.test.ts +++ b/packages/ui/src/shell/startupLocation.test.ts @@ -5,7 +5,7 @@ import { resolveStartupLocation, } from "./startupLocation"; -describe("startupLocation", () => { +describe("startup location", () => { afterEach(() => vi.restoreAllMocks()); it("restores the exact last location", async () => { diff --git a/packages/ui/src/router/startupLocation.ts b/packages/ui/src/shell/startupLocation.ts similarity index 92% rename from packages/ui/src/router/startupLocation.ts rename to packages/ui/src/shell/startupLocation.ts index c33ec091f7..8caca53f36 100644 --- a/packages/ui/src/router/startupLocation.ts +++ b/packages/ui/src/shell/startupLocation.ts @@ -3,7 +3,7 @@ import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersona import { toChannel } from "@posthog/ui/features/canvas/hooks/useChannels"; import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; -type ChannelClient = Pick< +type StartupLocationClient = Pick< PostHogAPIClient, "createDesktopFileSystemChannel" | "getDesktopFileSystemChannels" >; @@ -11,7 +11,7 @@ const storageKey = (identity: string): string => `startup-location:${identity}`; export async function resolveStartupLocation( identity: string, - client: ChannelClient, + client: StartupLocationClient, ): Promise { const saved = await rendererStateStorage.getItem(storageKey(identity)); return saved ?? (await personalNewTaskLocation(client)); @@ -22,7 +22,7 @@ export function rememberStartupLocation(identity: string, href: string): void { } export async function personalNewTaskLocation( - client: ChannelClient, + client: StartupLocationClient, ): Promise { const channels = (await client.getDesktopFileSystemChannels()) .filter((channel) => channel.type === "folder") From 1547521d333e642a7bf7362d24694e787cdeece1 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 19:48:10 +0200 Subject: [PATCH 5/8] Keep personal channel setup in canvas Generated-By: PostHog Code Task-Id: c6ae4672-c246-4078-857d-b00e81aaaaaa --- .../canvas/ensurePersonalChannel.test.ts | 15 +++++--- .../features/canvas/ensurePersonalChannel.ts | 37 ++++++++++++++++--- .../src/features/canvas/hooks/useChannels.ts | 2 +- packages/ui/src/shell/startupLocation.test.ts | 23 ++---------- packages/ui/src/shell/startupLocation.ts | 29 ++++----------- 5 files changed, 53 insertions(+), 53 deletions(-) diff --git a/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts b/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts index 655faf65cc..2eb9609aac 100644 --- a/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts +++ b/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts @@ -1,9 +1,11 @@ -import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels"; import { beforeEach, expect, it, vi } from "vitest"; -import { ensurePersonalChannel } from "./ensurePersonalChannel"; +import { + ensurePersonalChannel, + type PersonalChannel, +} from "./ensurePersonalChannel"; -function channel(id: string, name = "me"): Channel { - return { id, name, path: `/${name}`, type: "folder" } as Channel; +function channel(id: string, name = "me"): PersonalChannel { + return { id, name }; } // The module memoises the created folder, so each test needs a fresh copy. @@ -25,7 +27,8 @@ it("shares one create between callers racing before it settles", async () => { "./ensurePersonalChannel" ); const create = vi.fn( - () => new Promise((r) => setTimeout(() => r(channel("1")), 5)), + () => + new Promise((r) => setTimeout(() => r(channel("1")), 5)), ); const [a, b] = await Promise.all([ensure([], create), ensure([], create)]); @@ -69,7 +72,7 @@ it("lets a later caller retry after a failed create", async () => { "./ensurePersonalChannel" ); const create = vi - .fn<() => Promise>() + .fn<() => Promise>() .mockRejectedValueOnce(new Error("offline")) .mockResolvedValueOnce(channel("1")); diff --git a/packages/ui/src/features/canvas/ensurePersonalChannel.ts b/packages/ui/src/features/canvas/ensurePersonalChannel.ts index 8d1fe49dc9..eb46937c11 100644 --- a/packages/ui/src/features/canvas/ensurePersonalChannel.ts +++ b/packages/ui/src/features/canvas/ensurePersonalChannel.ts @@ -1,19 +1,29 @@ -import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels"; +import type { PostHogAPIClient } from "@posthog/api-client/posthog-client"; import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; +export interface PersonalChannel { + id: string; + name: string; +} + +export type PersonalChannelClient = Pick< + PostHogAPIClient, + "createDesktopFileSystemChannel" | "getDesktopFileSystemChannels" +>; + // The "me" folder is provisioned on first use, and folder creation is not // server-side idempotent by path — so two callers racing before the first // create lands in the channels cache would each make their own "me". The entry // points are trivially concurrent (Cmd+T's new tab, the sidebar row, its "+" // menu), so they share one in-flight create rather than guarding separately: // per-caller guards would still race each other. -let inFlight: Promise | null = null; +let inFlight: Promise | null = null; // The in-flight promise alone isn't enough: it settles the moment the POST // returns, but callers pass the `channels` from their last render, which hasn't // re-rendered with the seeded cache yet. A click landing in that gap sees // neither an existing "me" nor an in-flight create, and makes a second one. // Remember what was created until the list catches up. -let created: Channel | null = null; +let created: PersonalChannel | null = null; /** * The user's "me" folder, creating it once if it doesn't exist yet. Concurrent @@ -21,9 +31,9 @@ let created: Channel | null = null; * messaging. */ export async function ensurePersonalChannel( - channels: readonly Channel[], - createChannel: (name: string) => Promise, -): Promise { + channels: readonly PersonalChannel[], + createChannel: (name: string) => Promise, +): Promise { const existing = channels.find((c) => c.name === PERSONAL_CHANNEL_NAME); if (existing) { // The list is authoritative once it carries the folder: drop the memo, so a @@ -44,3 +54,18 @@ export async function ensurePersonalChannel( } return inFlight; } + +export async function ensurePersonalChannelFromClient( + client: PersonalChannelClient, +): Promise { + const toPersonalChannel = ({ id, path }: { id: string; path: string }) => ({ + id, + name: path.replace(/^\/+/, ""), + }); + const channels = (await client.getDesktopFileSystemChannels()) + .filter((channel) => channel.type === "folder") + .map(toPersonalChannel); + return await ensurePersonalChannel(channels, async (name) => + toPersonalChannel(await client.createDesktopFileSystemChannel(name)), + ); +} diff --git a/packages/ui/src/features/canvas/hooks/useChannels.ts b/packages/ui/src/features/canvas/hooks/useChannels.ts index ca3f05b7de..ba2e78945a 100644 --- a/packages/ui/src/features/canvas/hooks/useChannels.ts +++ b/packages/ui/src/features/canvas/hooks/useChannels.ts @@ -26,7 +26,7 @@ export interface Channel { homeCanvasId?: string; } -export function toChannel(fs: Schemas.FileSystem): Channel { +function toChannel(fs: Schemas.FileSystem): Channel { // The generated OpenAPI type declares `meta` as null, but the API returns our // free-form blob at runtime; read homeCanvasId past the type. const meta = fs.meta as { homeCanvasId?: string } | null | undefined; diff --git a/packages/ui/src/shell/startupLocation.test.ts b/packages/ui/src/shell/startupLocation.test.ts index 267ef92695..eb518f4af6 100644 --- a/packages/ui/src/shell/startupLocation.test.ts +++ b/packages/ui/src/shell/startupLocation.test.ts @@ -1,9 +1,6 @@ import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - personalNewTaskLocation, - resolveStartupLocation, -} from "./startupLocation"; +import { resolveStartupLocation } from "./startupLocation"; describe("startup location", () => { afterEach(() => vi.restoreAllMocks()); @@ -21,7 +18,8 @@ describe("startup location", () => { expect(client.getDesktopFileSystemChannels).not.toHaveBeenCalled(); }); - it("opens a new task in an existing me space", async () => { + it("opens a new task in me when there is no saved location", async () => { + vi.spyOn(rendererStateStorage, "getItem").mockResolvedValue(null); const client = { getDesktopFileSystemChannels: vi .fn() @@ -29,22 +27,9 @@ describe("startup location", () => { createDesktopFileSystemChannel: vi.fn(), }; - await expect(personalNewTaskLocation(client)).resolves.toBe( + await expect(resolveStartupLocation("project", client)).resolves.toBe( "/website/me-id/new", ); expect(client.createDesktopFileSystemChannel).not.toHaveBeenCalled(); }); - - it("creates the me space on first use", async () => { - const client = { - getDesktopFileSystemChannels: vi.fn().mockResolvedValue([]), - createDesktopFileSystemChannel: vi - .fn() - .mockResolvedValue({ id: "new-me-id", path: "me", type: "folder" }), - }; - - await expect(personalNewTaskLocation(client)).resolves.toBe( - "/website/new-me-id/new", - ); - }); }); diff --git a/packages/ui/src/shell/startupLocation.ts b/packages/ui/src/shell/startupLocation.ts index 8caca53f36..1f2bf43b69 100644 --- a/packages/ui/src/shell/startupLocation.ts +++ b/packages/ui/src/shell/startupLocation.ts @@ -1,34 +1,21 @@ -import type { PostHogAPIClient } from "@posthog/api-client/posthog-client"; -import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersonalChannel"; -import { toChannel } from "@posthog/ui/features/canvas/hooks/useChannels"; +import { + ensurePersonalChannelFromClient, + type PersonalChannelClient, +} from "@posthog/ui/features/canvas/ensurePersonalChannel"; import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; -type StartupLocationClient = Pick< - PostHogAPIClient, - "createDesktopFileSystemChannel" | "getDesktopFileSystemChannels" ->; const storageKey = (identity: string): string => `startup-location:${identity}`; export async function resolveStartupLocation( identity: string, - client: StartupLocationClient, + client: PersonalChannelClient, ): Promise { const saved = await rendererStateStorage.getItem(storageKey(identity)); - return saved ?? (await personalNewTaskLocation(client)); + if (saved) return saved; + const personal = await ensurePersonalChannelFromClient(client); + return `/website/${personal.id}/new`; } export function rememberStartupLocation(identity: string, href: string): void { void rendererStateStorage.setItem(storageKey(identity), href); } - -export async function personalNewTaskLocation( - client: StartupLocationClient, -): Promise { - const channels = (await client.getDesktopFileSystemChannels()) - .filter((channel) => channel.type === "folder") - .map(toChannel); - const personal = await ensurePersonalChannel(channels, async (name) => - toChannel(await client.createDesktopFileSystemChannel(name)), - ); - return `/website/${personal.id}/new`; -} From 9781a3dc1b5de4031f347af7907e01e1e192a5f0 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 20:08:55 +0200 Subject: [PATCH 6/8] Use a concise state storage name Generated-By: PostHog Code Task-Id: c6ae4672-c246-4078-857d-b00e81aaaaaa --- packages/ui/src/shell/rendererStorage.ts | 4 ++-- packages/ui/src/shell/startupLocation.test.ts | 6 +++--- packages/ui/src/shell/startupLocation.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/shell/rendererStorage.ts b/packages/ui/src/shell/rendererStorage.ts index 9d17b3c97b..dcb70ac353 100644 --- a/packages/ui/src/shell/rendererStorage.ts +++ b/packages/ui/src/shell/rendererStorage.ts @@ -118,7 +118,7 @@ export function registerRendererStateStorage( hostStorageReady.resolve(storage); } -export const rendererStateStorage: StateStorage = { +export const stateStorage: StateStorage = { getItem: async (key) => { // A coalesced write that has not flushed yet is newer than the backend // copy; land it first so the read never observes older state. A queued @@ -170,4 +170,4 @@ export const rendererStateStorage: StateStorage = { }, }; -export const electronStorage = createJSONStorage(() => rendererStateStorage); +export const electronStorage = createJSONStorage(() => stateStorage); diff --git a/packages/ui/src/shell/startupLocation.test.ts b/packages/ui/src/shell/startupLocation.test.ts index eb518f4af6..91934cbcf0 100644 --- a/packages/ui/src/shell/startupLocation.test.ts +++ b/packages/ui/src/shell/startupLocation.test.ts @@ -1,4 +1,4 @@ -import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; +import { stateStorage } from "@posthog/ui/shell/rendererStorage"; import { afterEach, describe, expect, it, vi } from "vitest"; import { resolveStartupLocation } from "./startupLocation"; @@ -6,7 +6,7 @@ describe("startup location", () => { afterEach(() => vi.restoreAllMocks()); it("restores the exact last location", async () => { - vi.spyOn(rendererStateStorage, "getItem").mockResolvedValue("/code"); + vi.spyOn(stateStorage, "getItem").mockResolvedValue("/code"); const client = { getDesktopFileSystemChannels: vi.fn(), createDesktopFileSystemChannel: vi.fn(), @@ -19,7 +19,7 @@ describe("startup location", () => { }); it("opens a new task in me when there is no saved location", async () => { - vi.spyOn(rendererStateStorage, "getItem").mockResolvedValue(null); + vi.spyOn(stateStorage, "getItem").mockResolvedValue(null); const client = { getDesktopFileSystemChannels: vi .fn() diff --git a/packages/ui/src/shell/startupLocation.ts b/packages/ui/src/shell/startupLocation.ts index 1f2bf43b69..bafa4a491b 100644 --- a/packages/ui/src/shell/startupLocation.ts +++ b/packages/ui/src/shell/startupLocation.ts @@ -2,7 +2,7 @@ import { ensurePersonalChannelFromClient, type PersonalChannelClient, } from "@posthog/ui/features/canvas/ensurePersonalChannel"; -import { rendererStateStorage } from "@posthog/ui/shell/rendererStorage"; +import { stateStorage } from "@posthog/ui/shell/rendererStorage"; const storageKey = (identity: string): string => `startup-location:${identity}`; @@ -10,12 +10,12 @@ export async function resolveStartupLocation( identity: string, client: PersonalChannelClient, ): Promise { - const saved = await rendererStateStorage.getItem(storageKey(identity)); + const saved = await stateStorage.getItem(storageKey(identity)); if (saved) return saved; const personal = await ensurePersonalChannelFromClient(client); return `/website/${personal.id}/new`; } export function rememberStartupLocation(identity: string, href: string): void { - void rendererStateStorage.setItem(storageKey(identity), href); + void stateStorage.setItem(storageKey(identity), href); } From 412ed35fc3d907c33d1d9dc87fa4030f41823c95 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Fri, 31 Jul 2026 10:26:20 +0200 Subject: [PATCH 7/8] fix(shell): scope personal channel startup cache Generated-By: PostHog Code Task-Id: c6ae4672-c246-4078-857d-b00e81aaaaaa --- .../canvas/ensurePersonalChannel.test.ts | 12 +++++++ .../features/canvas/ensurePersonalChannel.ts | 33 ++++++++++++------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts b/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts index 2eb9609aac..6cc4f69e06 100644 --- a/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts +++ b/packages/ui/src/features/canvas/ensurePersonalChannel.test.ts @@ -80,3 +80,15 @@ it("lets a later caller retry after a failed create", async () => { await expect(ensure([], create)).resolves.toEqual(channel("1")); expect(create).toHaveBeenCalledTimes(2); }); + +it("does not share a created folder between scopes", async () => { + const { ensurePersonalChannel: ensure } = await import( + "./ensurePersonalChannel" + ); + const createFirst = vi.fn(async () => channel("1")); + const createSecond = vi.fn(async () => channel("2")); + + await expect(ensure([], createFirst, {})).resolves.toEqual(channel("1")); + await expect(ensure([], createSecond, {})).resolves.toEqual(channel("2")); + expect(createSecond).toHaveBeenCalledOnce(); +}); diff --git a/packages/ui/src/features/canvas/ensurePersonalChannel.ts b/packages/ui/src/features/canvas/ensurePersonalChannel.ts index eb46937c11..9dcb8cac56 100644 --- a/packages/ui/src/features/canvas/ensurePersonalChannel.ts +++ b/packages/ui/src/features/canvas/ensurePersonalChannel.ts @@ -17,13 +17,18 @@ export type PersonalChannelClient = Pick< // points are trivially concurrent (Cmd+T's new tab, the sidebar row, its "+" // menu), so they share one in-flight create rather than guarding separately: // per-caller guards would still race each other. -let inFlight: Promise | null = null; +interface PersonalChannelState { + inFlight: Promise | null; + created: PersonalChannel | null; +} + +const sharedScope = {}; +const stateByScope = new WeakMap(); // The in-flight promise alone isn't enough: it settles the moment the POST // returns, but callers pass the `channels` from their last render, which hasn't // re-rendered with the seeded cache yet. A click landing in that gap sees // neither an existing "me" nor an in-flight create, and makes a second one. // Remember what was created until the list catches up. -let created: PersonalChannel | null = null; /** * The user's "me" folder, creating it once if it doesn't exist yet. Concurrent @@ -33,26 +38,29 @@ let created: PersonalChannel | null = null; export async function ensurePersonalChannel( channels: readonly PersonalChannel[], createChannel: (name: string) => Promise, + scope: object = sharedScope, ): Promise { + const state = stateByScope.get(scope) ?? { inFlight: null, created: null }; + stateByScope.set(scope, state); const existing = channels.find((c) => c.name === PERSONAL_CHANNEL_NAME); if (existing) { // The list is authoritative once it carries the folder: drop the memo, so a // deleted-then-recreated "me" resolves fresh rather than to a dead id. - created = null; + state.created = null; return existing; } - if (created) return created; - if (!inFlight) { - inFlight = createChannel(PERSONAL_CHANNEL_NAME) + if (state.created) return state.created; + if (!state.inFlight) { + state.inFlight = createChannel(PERSONAL_CHANNEL_NAME) .then((channel) => { - created = channel; + state.created = channel; return channel; }) .finally(() => { - inFlight = null; + state.inFlight = null; }); } - return inFlight; + return state.inFlight; } export async function ensurePersonalChannelFromClient( @@ -65,7 +73,10 @@ export async function ensurePersonalChannelFromClient( const channels = (await client.getDesktopFileSystemChannels()) .filter((channel) => channel.type === "folder") .map(toPersonalChannel); - return await ensurePersonalChannel(channels, async (name) => - toPersonalChannel(await client.createDesktopFileSystemChannel(name)), + return await ensurePersonalChannel( + channels, + async (name) => + toPersonalChannel(await client.createDesktopFileSystemChannel(name)), + client, ); } From f10c73a622ddbf389b489e029fec3e3370219033 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:36:14 +0000 Subject: [PATCH 8/8] chore(visual): update storybook baselines 6 updated, 4 removed Run: e0e79852-4378-4765-b314-bf9b47cebef4 Co-authored-by: puemos <13174025+puemos@users.noreply.github.com> --- apps/code/snapshots.yml | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index 94a33aa7ea..843bb26229 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -537,21 +537,17 @@ snapshots: git-dialogs--sync-success--light: hash: v1.k4693efd2.8d79d77c9338aeaa73734836f0e17fde987f6d3239914f4013889d3110e5088d.Rq-w2o1uhes_a5hUWqMXeiDkfnoPCZPt932LkEQXCco loops-loopslistview--comprehensive--dark: - hash: v1.k4693efd2.c128012b7f910839e65e8eaf14d5456c93f90d77747d0625ca35fddd8d5adf7a.fuugB5Ov6boP7fg6fWP7A7IG2yYnPUAAWLKZLuV8rZ0 + hash: v1.k4693efd2.25744a9471ec664c70e346ac5cb330c025a031626bc486abe8820b7b6dbc1a19.Shh8Yl7SdombnNc4-uJjlIbx-n4C0MGXxtOGB1bjJTY loops-loopslistview--comprehensive--light: - hash: v1.k4693efd2.4d25a48e0bd7e60d3bbb433e246486e4c9ac95979db86e20eb8bf5cb119d1eb2.EzrENjVHwVMyV00pEA-71TeVomJtchiym5DqWFkujlA + hash: v1.k4693efd2.ef8e012cc506a0bf5e761ec8e89943820d4b391f7023f506da7d79586b49a356.ATiN8Y0hUmfGvr0rUytYWX-fITyHRTgeN1i_HOOGZuc loops-loopslistview--long-mixed-list--dark: - hash: v1.k4693efd2.fdbf0c24cb386c4345279db3e648efe72aa8a2a984d491361522f62074a771c9.2QmRxRMWYFFfBFrbMm0Dm9rWcvd1lfRbc0egfzoESnY + hash: v1.k4693efd2.c9915e22655fe1f6b77eabaf11d8876648c387ceebac6cd8be1880e7255bd7a5.y-gyTKSp7wYbuS57TABRbEyMSL9_U1wvHYvbyCeAbiQ loops-loopslistview--long-mixed-list--light: - hash: v1.k4693efd2.aa17d9b6f40fbd3914bd742951db9f90add0bd233c54170803c8483aeb45b43b.yXpqqGczOq1uW5k4Ie7ddNTnne-cFTglZ9cL800pXB4 - loops-loopslistview--shared-page-header--dark: - hash: v1.k4693efd2.14ab860dcb0df51b4019f012288e45f3a2c4c23fa08e809efef8c04b5687fe36.qoMLHprZmV2wON156NBbl3sqP84a4nXgdYm1H6WpTVM - loops-loopslistview--shared-page-header--light: - hash: v1.k4693efd2.23be5408ff3b4a912a0ce0457af8638124b99a01c88a499d0f4204cee2c62487.dGBP1K08MA2Fvhml-BPl5eFra8U83JXmy04NK9wOQnM + hash: v1.k4693efd2.5fec9e1a3c14f1bc319cd7e5abc24b3d66ed787986a103b5330f49341d4983ad.mpD-eTS6vkSbsJdOYaHXASHQd5IiJGWRCb6uDGlzhpk loops-loopslistview--with-builder-sessions--dark: - hash: v1.k4693efd2.df4a1a4764e1de7b69905814b09c30d9b43f705f5e66c7ad83829c567d4faa1a.jthufoX_KPbbxScjj3pPBA4lEtT9Ym6zUCH-v1759GM + hash: v1.k4693efd2.06b404e4016ba0eb86769331a574bc640be1ad26ebf3a04d0256334d1da041a3.fPfF8c2wNFREfy8dVRdsw7W9Fpqj7HEtuaERMroNHMA loops-loopslistview--with-builder-sessions--light: - hash: v1.k4693efd2.eb65c38bb0de7efedbc0d7a6c27a6695528e2e6b60949617e7d22335b1e68c8b.fJr9uykvc8S7LI_ASXk5KRqxXCwIqh6ld9zfzs4shP0 + hash: v1.k4693efd2.30f89915a12cadf313216403fb0f3e221f3c33cee763f0692ae2b96b4332e2b9.j00vexnH4QS0wJAHn7pQNpeITYYRtOxlvB2ngyzqU0c primitives-pageheader--title-only--dark: hash: v1.k4693efd2.f7f8c560b1e5cf20050189e10fc0f129e555ba672300a7a09bef5827c171a743.yP9Pp_k51agu_9Ri55Y7uCAPBQ1UTeUQJA9U9dPMKfQ primitives-pageheader--title-only--light: @@ -696,10 +692,6 @@ snapshots: hash: v1.k4693efd2.e747a3b5d0983a7a125335f1d95cc7735dafda73abdb3f63ccfadde0482c5ce9.tjc4lMKeaOJjLLxd9tivmgKLshzHZwuSAa_MbqwN_20 skill-buttons-skillbuttonactionmessage--run-experiment--light: hash: v1.k4693efd2.c18264887c67224ea1196646ff27f31ef9d79add87b5e9dfead377fcaf0dc941.MwzR7Yyd4le31Qm7RB2yk-qLk5ujgLkN-cMAB2WQ9_k - skill-buttons-skillbuttonsmenu--default--dark: - hash: v1.k4693efd2.cfff4c6bbb0acef1c9941b21d2f69c24b234c2c206282acf6fcada0cb2bd2386.xILjVPp8ecQmGbPjGDdhbRyi9KFPpA_1nfYF3bfjwuc - skill-buttons-skillbuttonsmenu--default--light: - hash: v1.k4693efd2.eb9bd49b9700641f6f3c90653447c9767c52e997afec9843e8649b90752faad3.VdxEtBwtkU3ioy2evcNbrEKxPjQ1GDUpZAQzFe-Uho4 spaces-taskfeedrow--agent-origin--dark: hash: v1.k4693efd2.82f8c70a399c9ea768201933e202fcc2fde74332c5153aeabf6835ace79beee6.yEZq_qnKxItv5u0Ydcqa1B4VCwv8lwXUvcaFScE2XO0 spaces-taskfeedrow--agent-origin--light: