Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/code/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,10 @@ snapshots:
hash: v1.k4693efd2.384486583f8d9b66a4bf1d9cc45036a07310cb9e1943ea5b52ff466f55a03d7b.F_sXrJ13XWHzAF8yhoKun_k4BY8Jcb3EDtT0L1I6nKw
loops-loopslistview--long-mixed-list--light:
hash: v1.k4693efd2.d80a0c55c4490276df53f781ac88ed9d9c6ec282bc14ddb80140b3ddcc44d32f.L6hw9ZixUiyTFKOybYzmjfUSIOayvmhueMTcmYt-IWo
loops-loopslistview--with-builder-sessions--dark:
hash: v1.k4693efd2.bc59812bf49ff161cf20e75dcf043367ae3d2817aa2c2cf91c91d9771c6b62f8.yOBz-llmwDVay7GIHlMwlMT3EuKjkoW0Vwmwvi4noCM
loops-loopslistview--with-builder-sessions--light:
hash: v1.k4693efd2.fb97e3fe3205ddf71d98c4e35446c73e394aaa9761d3bb35b146ddafc438df4d.UUEWc0G12NQibZTyskIHGS62mB36gKJ594GrJp1E9II
scouts-scoutsfleetlist--filtered-to-you--dark:
hash: v1.k4693efd2.a3af6e76ef36598eaa347bedc4430878ed62754660166398e0576a9978cd084b.x3Ky-O6HOw0srWdOmnnKJwFNpmGmQVWip0t7CwVaRXY
scouts-scoutsfleetlist--filtered-to-you--light:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArrowUp, Stop } from "@phosphor-icons/react";
import { ArrowUp, StopCircle } from "@phosphor-icons/react";
import {
InputGroup,
InputGroupAddon,
Expand Down Expand Up @@ -179,7 +179,7 @@ function Composer({
onClick={onCancel}
aria-label="Stop"
>
<Stop size={14} weight="fill" />
<StopCircle size={14} weight="fill" />
</InputGroupButton>
</Tooltip>
) : (
Expand Down
10 changes: 8 additions & 2 deletions packages/ui/src/features/autoresearch/AutoresearchPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ChartLineUp, Pause, Play, Plus, Stop } from "@phosphor-icons/react";
import {
ChartLineUp,
Pause,
Play,
Plus,
StopCircle,
} from "@phosphor-icons/react";
import type { AutoresearchService } from "@posthog/core/autoresearch/autoresearch";
import { AUTORESEARCH_SERVICE } from "@posthog/core/autoresearch/identifiers";
import type {
Expand Down Expand Up @@ -344,7 +350,7 @@ function RunHeader({
color="red"
onClick={() => service.stopRun(run.id)}
>
<Stop size={12} /> Stop
<StopCircle size={12} /> Stop
</Button>
)}
{!isLive && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
defaultEligibleModel,
getCloudUrlFromRegion,
} from "@posthog/shared";
import type { Task } from "@posthog/shared/domain-types";
import { useAuthStateValue } from "@posthog/ui/features/auth/store";
import { showOfflineToast } from "@posthog/ui/features/connectivity/connectivityToast";
import { resolveDefaultModel } from "@posthog/ui/features/inbox/hooks/resolveDefaultModel";
Expand Down Expand Up @@ -90,6 +91,8 @@ export interface UseInboxCloudTaskRunnerOptions {
buildInput: (ctx: InboxCloudTaskInputContext) => TaskCreationInput;
/** Telemetry extras merged into the TASK_CREATED event when the run succeeds. */
analyticsExtras?: Record<string, unknown>;
/** Called with the created task record, before any navigation happens. */
onTaskCreated?: (task: Task) => void;
/**
* When false, the runner does not navigate to the created task. The task is
* still added to the sidebar via `invalidateTasks`, and a success toast with a
Expand Down Expand Up @@ -120,6 +123,7 @@ export function useInboxCloudTaskRunner({
loggerScope,
buildInput,
analyticsExtras,
onTaskCreated,
redirectOnSuccess = true,
}: UseInboxCloudTaskRunnerOptions): UseInboxCloudTaskRunnerReturn {
const [isRunning, setIsRunning] = useState(false);
Expand Down Expand Up @@ -225,6 +229,7 @@ export function useInboxCloudTaskRunner({
const result = await taskService.createTask(input, (output) => {
createdTask = output.task;
invalidateTasks(output.task);
onTaskCreated?.(output.task);
if (redirectOnSuccess) {
void openTask(output.task);
}
Expand Down Expand Up @@ -299,6 +304,7 @@ export function useInboxCloudTaskRunner({
buildInput,
copy,
analyticsExtras,
onTaskCreated,
modelResolver,
taskService,
redirectOnSuccess,
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/src/features/loops/components/LoopRunRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
type Icon,
Lightning,
Play,
Stop,
Timer,
Warning,
X,
Expand Down Expand Up @@ -188,7 +187,6 @@ export function LoopRunRow({
size="1"
onClick={() => setStopOpen(true)}
>
<Stop size={12} weight="bold" />
Stop run
</Button>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,24 @@ export const LongMixedList: Story = {
}),
},
};

export const WithBuilderSessions: Story = {
args: {
builderSessions: [
{
taskId: "builder-task-1",
prompt: "Summarize my open PRs every weekday morning",
startedAt: 1752000000000,
identity: "us:2",
},
{
taskId: "builder-task-2",
prompt: "Build a loop",
startedAt: 1752000600000,
identity: "us:2",
},
],
onResumeBuilderSession: () => {},
onBuilderSessionStopped: () => {},
},
};
105 changes: 103 additions & 2 deletions packages/ui/src/features/loops/components/LoopsListView.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { CloudIcon, PlusIcon, RepeatIcon } from "@phosphor-icons/react";
import {
ChatCircleDotsIcon,
CloudIcon,
PlusIcon,
RepeatIcon,
} from "@phosphor-icons/react";
import type { LoopSchemas } from "@posthog/api-client/loops";
import type { UserBasic } from "@posthog/shared/domain-types";
import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers";
import { StopCloudRunDialog } from "@posthog/ui/features/sessions/components/StopCloudRunDialog";
import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent";
import { Button } from "@posthog/ui/primitives/Button";
import { navigateToNewLoop } from "@posthog/ui/router/navigationBridge";
import { toast } from "@posthog/ui/primitives/toast";
import {
navigateToNewLoop,
navigateToTaskDetail,
} from "@posthog/ui/router/navigationBridge";
import { Flex, Heading, Text } from "@radix-ui/themes";
import { useMemo, useState } from "react";
import { useLoopBuilderSessions } from "../hooks/useLoopBuilderSessions";
import { useLoopLimits, useLoops } from "../hooks/useLoops";
import {
type LoopBuilderSession,
useLoopBuilderSessionStore,
} from "../loopBuilderSessionStore";
import { useLoopDraftStore } from "../loopDraftStore";
import type { LoopTemplate } from "../loopTemplates";
import { LoopBuilderComposer } from "./LoopBuilderComposer";
Expand All @@ -23,6 +38,7 @@ function loopLimitReason(max: number): string {
}

const EMPTY_MEMBERS: UserBasic[] = [];
const EMPTY_BUILDER_SESSIONS: LoopBuilderSession[] = [];

const SECTION_PREVIEW_COUNT = 5;

Expand All @@ -31,6 +47,14 @@ function startBlankLoop(): void {
navigateToNewLoop();
}

function resumeBuilderSession(taskId: string): void {
navigateToTaskDetail(taskId);
}

function removeBuilderSession(taskId: string): void {
useLoopBuilderSessionStore.getState().removeSession(taskId);
}

function startLoopFromTemplate(template: LoopTemplate): void {
useLoopDraftStore
.getState()
Expand Down Expand Up @@ -60,6 +84,8 @@ export function LoopsListView() {
);
useSetHeaderContent(headerContent);

const builderSessions = useLoopBuilderSessions();

const allLoops = loops ?? [];
const teamLoops = allLoops.filter((loop) => loop.visibility === "team");
const {
Expand All @@ -79,8 +105,11 @@ export function LoopsListView() {
membersLoading={membersLoading}
membersError={membersError}
membersComplete={membersComplete}
builderSessions={builderSessions}
onStartBlank={startBlankLoop}
onStartFromTemplate={startLoopFromTemplate}
onResumeBuilderSession={resumeBuilderSession}
onBuilderSessionStopped={removeBuilderSession}
/>
);
}
Expand All @@ -94,8 +123,11 @@ interface LoopsListViewPresentationProps {
membersLoading?: boolean;
membersError?: boolean;
membersComplete?: boolean;
builderSessions?: LoopBuilderSession[];
onStartBlank: () => void;
onStartFromTemplate: (template: LoopTemplate) => void;
onResumeBuilderSession?: (taskId: string) => void;
onBuilderSessionStopped?: (taskId: string) => void;
}

export function LoopsListViewPresentation({
Expand All @@ -107,8 +139,11 @@ export function LoopsListViewPresentation({
membersLoading = false,
membersError = false,
membersComplete = true,
builderSessions = EMPTY_BUILDER_SESSIONS,
onStartBlank,
onStartFromTemplate,
onResumeBuilderSession,
onBuilderSessionStopped,
}: LoopsListViewPresentationProps) {
const personalLoops = loops.filter((loop) => loop.visibility === "personal");
const teamLoops = loops.filter((loop) => loop.visibility === "team");
Expand Down Expand Up @@ -199,13 +234,79 @@ export function LoopsListViewPresentation({
gap="2"
className="mx-auto w-full max-w-5xl px-8 pb-6"
>
{builderSessions.map((session) => (
<BuilderSessionRow
key={session.taskId}
session={session}
onResume={onResumeBuilderSession}
onStopped={onBuilderSessionStopped}
/>
))}
<LoopBuilderComposer disabledReason={limitReason} />
</Flex>
</div>
</Flex>
);
}

function BuilderSessionRow({
session,
onResume,
onStopped,
}: {
session: LoopBuilderSession;
onResume?: (taskId: string) => void;
onStopped?: (taskId: string) => void;
}) {
const [confirmStop, setConfirmStop] = useState(false);

return (
<Flex
align="center"
gap="3"
className="rounded-(--radius-2) border border-border bg-(--color-panel-solid) px-3 py-2"
>
<ChatCircleDotsIcon size={16} className="shrink-0 text-(--accent-11)" />
<Flex direction="column" className="min-w-0 flex-1">
<Text className="font-medium text-[12px] text-gray-10 uppercase tracking-wide">
Builder in progress
</Text>
<Text className="truncate text-[13px] text-gray-12">
{session.prompt}
</Text>
</Flex>
<Button
variant="soft"
color="red"
size="1"
onClick={() => setConfirmStop(true)}
>
Stop
</Button>
<Button
variant="soft"
size="1"
onClick={() => onResume?.(session.taskId)}
>
Resume
</Button>
{confirmStop ? (
<StopCloudRunDialog
open={confirmStop}
taskId={session.taskId}
title="Stop loop builder"
buttonLabel="Stop builder"
onOpenChange={setConfirmStop}
onStopped={() => {
toast.success("Builder stopped");
onStopped?.(session.taskId);
}}
/>
) : null}
</Flex>
);
}

function LoopListSection({
title,
loops,
Expand Down
Loading
Loading