From 7e6ab06c84487519bd8b19da4d07c1a345d8b610 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 29 Jul 2026 02:37:00 -0700 Subject: [PATCH 1/4] restore templates on space loops tab --- .../canvas/components/WebsiteChannelLoops.tsx | 34 +++++++++++++++---- .../loops/components/LoopsEmptyState.tsx | 4 +-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx index 11a1e9f08f..38231afa59 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx @@ -12,11 +12,14 @@ import { } from "../../loops/components/LoopFallbacks"; import { LoopRow } from "../../loops/components/LoopRow"; import { LoopsEmptyState } from "../../loops/components/LoopsEmptyState"; +import { LoopTemplatesSection } from "../../loops/components/LoopTemplatesSection"; import { useLoopLimits, useLoops } from "../../loops/hooks/useLoops"; import { useLoopDraftStore } from "../../loops/loopDraftStore"; import { defaultLoopContextOutputs } from "../../loops/loopFormTypes"; +import type { LoopTemplate } from "../../loops/loopTemplates"; import { useChannels } from "../hooks/useChannels"; import { useOrgMembers } from "../hooks/useOrgMembers"; +import { PERSONAL_CHANNEL_NAME } from "../hooks/useTaskChannels"; function contextQuickStarts(name: string): { label: string; prompt: string }[] { return [ @@ -52,6 +55,7 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { const { channels } = useChannels(); const channel = channels.find((c) => c.id === channelId); const contextName = channel?.name ?? channelId; + const isPersonal = contextName === PERSONAL_CHANNEL_NAME; useSetHeaderContent( useMemo(() => , [channelId]), @@ -82,13 +86,26 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { navigateToNewLoop(); }; + const startFromTemplate = (template: LoopTemplate) => { + useLoopDraftStore.getState().setPrefill({ + description: template.description, + ...template.build(), + contextTarget: { + folderId: channelId, + name: contextName, + outputs: defaultLoopContextOutputs(), + }, + }); + navigateToNewLoop(); + }; + return (
- Automate #{contextName} + {isPersonal ? "Loops" : `Automate #${contextName}`} - Build a loop that posts its runs to this context's feed, or - keeps its context.md or a canvas up to date. + Put your work on autopilot. Loops run on a schedule, on an API + call, or when something happens on GitHub. You can finally close + the laptop!
@@ -169,7 +191,7 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { -
- +
+ Date: Wed, 29 Jul 2026 02:43:37 -0700 Subject: [PATCH 2/4] show ownership tabs on personal space loops --- .../canvas/components/WebsiteChannelLoops.tsx | 38 ++++++++++++++++--- .../loops/components/LoopsListView.tsx | 32 +++++++++++----- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx index 38231afa59..c7a0d8d9e1 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx @@ -1,4 +1,6 @@ import { CloudIcon, PlusIcon } from "@phosphor-icons/react"; +import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient"; +import { useCurrentUser } from "@posthog/ui/features/auth/useCurrentUser"; import { ChannelHeader } from "@posthog/ui/features/canvas/components/ChannelHeader"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { Button } from "@posthog/ui/primitives/Button"; @@ -12,6 +14,10 @@ import { } from "../../loops/components/LoopFallbacks"; import { LoopRow } from "../../loops/components/LoopRow"; import { LoopsEmptyState } from "../../loops/components/LoopsEmptyState"; +import { + LoopListTabs, + splitLoopsByOwnership, +} from "../../loops/components/LoopsListView"; import { LoopTemplatesSection } from "../../loops/components/LoopTemplatesSection"; import { useLoopLimits, useLoops } from "../../loops/hooks/useLoops"; import { useLoopDraftStore } from "../../loops/loopDraftStore"; @@ -56,11 +62,16 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { const channel = channels.find((c) => c.id === channelId); const contextName = channel?.name ?? channelId; const isPersonal = contextName === PERSONAL_CHANNEL_NAME; + const authenticatedClient = useOptionalAuthenticatedClient(); + const { data: currentUser, isLoading: currentUserLoading } = useCurrentUser({ + client: authenticatedClient, + }); useSetHeaderContent( useMemo(() => , [channelId]), ); + const allLoops = loops ?? []; const attachedLoops = useMemo( () => (loops ?? []).filter( @@ -68,12 +79,18 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { ), [loops, channelId], ); + const { personalLoops, teamLoops } = splitLoopsByOwnership( + allLoops, + currentUser?.id ?? null, + ); const { members, isLoading: membersLoading, isError: membersError, isComplete: membersComplete, - } = useOrgMembers({ enabled: attachedLoops.length > 0 }); + } = useOrgMembers({ + enabled: isPersonal ? allLoops.length > 0 : attachedLoops.length > 0, + }); const startBlank = () => { useLoopDraftStore.getState().setPrefill({ @@ -150,13 +167,26 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) {
- {isLoading ? ( + {isLoading || (isPersonal && currentUserLoading) ? ( ) : isError ? ( + ) : isPersonal ? ( + allLoops.length > 0 ? ( + + ) : ( + + ) ) : attachedLoops.length > 0 ? ( @@ -178,9 +208,7 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) {
) : ( - + )} diff --git a/packages/ui/src/features/loops/components/LoopsListView.tsx b/packages/ui/src/features/loops/components/LoopsListView.tsx index c8df964946..32c3bf6e2a 100644 --- a/packages/ui/src/features/loops/components/LoopsListView.tsx +++ b/packages/ui/src/features/loops/components/LoopsListView.tsx @@ -183,6 +183,24 @@ interface LoopsListViewPresentationProps { onBuilderSessionStopped?: (taskId: string) => void; } +export function splitLoopsByOwnership( + loops: LoopSchemas.Loop[], + currentUserId: number | null, +): { personalLoops: LoopSchemas.Loop[]; teamLoops: LoopSchemas.Loop[] } { + return { + personalLoops: loops.filter( + (loop) => + loop.visibility === "personal" || + (currentUserId !== null && loop.created_by_id === currentUserId), + ), + teamLoops: loops.filter( + (loop) => + loop.visibility === "team" && + (currentUserId === null || loop.created_by_id !== currentUserId), + ), + }; +} + export function LoopsListViewPresentation({ loops, currentUserId = null, @@ -199,15 +217,9 @@ export function LoopsListViewPresentation({ onResumeBuilderSession, onBuilderSessionStopped, }: LoopsListViewPresentationProps) { - const personalLoops = loops.filter( - (loop) => - loop.visibility === "personal" || - (currentUserId !== null && loop.created_by_id === currentUserId), - ); - const teamLoops = loops.filter( - (loop) => - loop.visibility === "team" && - (currentUserId === null || loop.created_by_id !== currentUserId), + const { personalLoops, teamLoops } = splitLoopsByOwnership( + loops, + currentUserId, ); return ( @@ -306,7 +318,7 @@ export function LoopsListViewPresentation({ ); } -function LoopListTabs({ +export function LoopListTabs({ personalLoops, teamLoops, members, From 245d4557716a89699065079858545d97dd73f7c9 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 29 Jul 2026 02:49:53 -0700 Subject: [PATCH 3/4] treat current-user failure as loops list error --- .../features/canvas/components/WebsiteChannelLoops.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx index c7a0d8d9e1..ca2338ee7f 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx @@ -63,9 +63,11 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { const contextName = channel?.name ?? channelId; const isPersonal = contextName === PERSONAL_CHANNEL_NAME; const authenticatedClient = useOptionalAuthenticatedClient(); - const { data: currentUser, isLoading: currentUserLoading } = useCurrentUser({ - client: authenticatedClient, - }); + const { + data: currentUser, + isLoading: currentUserLoading, + isError: currentUserError, + } = useCurrentUser({ client: authenticatedClient }); useSetHeaderContent( useMemo(() => , [channelId]), @@ -169,7 +171,7 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { {isLoading || (isPersonal && currentUserLoading) ? ( - ) : isError ? ( + ) : isError || (isPersonal && currentUserError) ? ( Date: Wed, 29 Jul 2026 02:59:02 -0700 Subject: [PATCH 4/4] drop ownership tabs from me loops tab --- .../canvas/components/WebsiteChannelLoops.tsx | 42 +++---------------- .../loops/components/LoopsListView.tsx | 32 +++++--------- 2 files changed, 16 insertions(+), 58 deletions(-) diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx index ca2338ee7f..38231afa59 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelLoops.tsx @@ -1,6 +1,4 @@ import { CloudIcon, PlusIcon } from "@phosphor-icons/react"; -import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient"; -import { useCurrentUser } from "@posthog/ui/features/auth/useCurrentUser"; import { ChannelHeader } from "@posthog/ui/features/canvas/components/ChannelHeader"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { Button } from "@posthog/ui/primitives/Button"; @@ -14,10 +12,6 @@ import { } from "../../loops/components/LoopFallbacks"; import { LoopRow } from "../../loops/components/LoopRow"; import { LoopsEmptyState } from "../../loops/components/LoopsEmptyState"; -import { - LoopListTabs, - splitLoopsByOwnership, -} from "../../loops/components/LoopsListView"; import { LoopTemplatesSection } from "../../loops/components/LoopTemplatesSection"; import { useLoopLimits, useLoops } from "../../loops/hooks/useLoops"; import { useLoopDraftStore } from "../../loops/loopDraftStore"; @@ -62,18 +56,11 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { const channel = channels.find((c) => c.id === channelId); const contextName = channel?.name ?? channelId; const isPersonal = contextName === PERSONAL_CHANNEL_NAME; - const authenticatedClient = useOptionalAuthenticatedClient(); - const { - data: currentUser, - isLoading: currentUserLoading, - isError: currentUserError, - } = useCurrentUser({ client: authenticatedClient }); useSetHeaderContent( useMemo(() => , [channelId]), ); - const allLoops = loops ?? []; const attachedLoops = useMemo( () => (loops ?? []).filter( @@ -81,18 +68,12 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) { ), [loops, channelId], ); - const { personalLoops, teamLoops } = splitLoopsByOwnership( - allLoops, - currentUser?.id ?? null, - ); const { members, isLoading: membersLoading, isError: membersError, isComplete: membersComplete, - } = useOrgMembers({ - enabled: isPersonal ? allLoops.length > 0 : attachedLoops.length > 0, - }); + } = useOrgMembers({ enabled: attachedLoops.length > 0 }); const startBlank = () => { useLoopDraftStore.getState().setPrefill({ @@ -169,26 +150,13 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) {
- {isLoading || (isPersonal && currentUserLoading) ? ( + {isLoading ? ( - ) : isError || (isPersonal && currentUserError) ? ( + ) : isError ? ( - ) : isPersonal ? ( - allLoops.length > 0 ? ( - - ) : ( - - ) ) : attachedLoops.length > 0 ? ( @@ -210,7 +178,9 @@ export function WebsiteChannelLoops({ channelId }: { channelId: string }) {
) : ( - + )} diff --git a/packages/ui/src/features/loops/components/LoopsListView.tsx b/packages/ui/src/features/loops/components/LoopsListView.tsx index 32c3bf6e2a..c8df964946 100644 --- a/packages/ui/src/features/loops/components/LoopsListView.tsx +++ b/packages/ui/src/features/loops/components/LoopsListView.tsx @@ -183,24 +183,6 @@ interface LoopsListViewPresentationProps { onBuilderSessionStopped?: (taskId: string) => void; } -export function splitLoopsByOwnership( - loops: LoopSchemas.Loop[], - currentUserId: number | null, -): { personalLoops: LoopSchemas.Loop[]; teamLoops: LoopSchemas.Loop[] } { - return { - personalLoops: loops.filter( - (loop) => - loop.visibility === "personal" || - (currentUserId !== null && loop.created_by_id === currentUserId), - ), - teamLoops: loops.filter( - (loop) => - loop.visibility === "team" && - (currentUserId === null || loop.created_by_id !== currentUserId), - ), - }; -} - export function LoopsListViewPresentation({ loops, currentUserId = null, @@ -217,9 +199,15 @@ export function LoopsListViewPresentation({ onResumeBuilderSession, onBuilderSessionStopped, }: LoopsListViewPresentationProps) { - const { personalLoops, teamLoops } = splitLoopsByOwnership( - loops, - currentUserId, + const personalLoops = loops.filter( + (loop) => + loop.visibility === "personal" || + (currentUserId !== null && loop.created_by_id === currentUserId), + ); + const teamLoops = loops.filter( + (loop) => + loop.visibility === "team" && + (currentUserId === null || loop.created_by_id !== currentUserId), ); return ( @@ -318,7 +306,7 @@ export function LoopsListViewPresentation({ ); } -export function LoopListTabs({ +function LoopListTabs({ personalLoops, teamLoops, members,