Skip to content

Commit 9440587

Browse files
committed
updates
1 parent ce2a4ee commit 9440587

6 files changed

Lines changed: 178 additions & 14 deletions

File tree

packages/ui/src/features/loops/components/LoopBehaviorFields.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export function LoopBehaviorFields({
2626
Auto-fix pull requests
2727
</Text>
2828
<Text className="text-[12px] text-gray-10">
29-
Watch CI and review comments on PRs this loop opens, and let Claude
30-
push fixes.
29+
Watch CI and review comments on PRs this loop opens, and let
30+
PostHog push fixes.
3131
</Text>
3232
</Flex>
3333
<Switch

packages/ui/src/features/loops/components/LoopForm.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type LoopContextTargetDraft,
2828
type LoopFormValues,
2929
loopToFormValues,
30+
normalizeLoopFormValues,
3031
} from "../loopFormTypes";
3132
import { LoopBehaviorFields } from "./LoopBehaviorFields";
3233
import { LoopContextFields } from "./LoopContextFields";
@@ -60,12 +61,15 @@ export function LoopForm({ loop }: LoopFormProps) {
6061
const isEdit = !!loop;
6162
const projectId = useAuthStateValue((state) => state.currentProjectId);
6263
const [values, setValues] = useState<LoopFormValues>(() => {
63-
if (loop) return loopToFormValues(loop);
64+
if (loop) return normalizeLoopFormValues(loopToFormValues(loop));
6465
// One-shot prefill from the landing prompt or a template; merged over the
6566
// blank defaults. Read (not consumed) here, then cleared in the effect
6667
// below so the manual "New loop" button always opens a blank form.
6768
const prefill = useLoopDraftStore.getState().prefill;
68-
return { ...emptyLoopFormValues(), ...(prefill ?? {}) };
69+
return normalizeLoopFormValues({
70+
...emptyLoopFormValues(),
71+
...(prefill ?? {}),
72+
});
6973
});
7074
const [step, setStep] = useState(0);
7175
// Open when editing a loop that already pins a model, so the pinned value
@@ -200,11 +204,19 @@ export function LoopForm({ loop }: LoopFormProps) {
200204
title="Options"
201205
description="Who can see it and how you hear about runs."
202206
>
203-
<Field label="Visibility" className="max-w-[340px]">
207+
<Field
208+
label="Visibility"
209+
className="max-w-[340px]"
210+
hint={
211+
values.contextTarget
212+
? "Loops attached to a context post runs to its shared feed, so they're visible to everyone on the project."
213+
: undefined
214+
}
215+
>
204216
<SettingsOptionSelect
205217
value={values.visibility}
206218
options={VISIBILITY_OPTIONS}
207-
disabled={isSubmitting}
219+
disabled={isSubmitting || !!values.contextTarget}
208220
ariaLabel="Visibility"
209221
onValueChange={(value) =>
210222
patch({
@@ -223,7 +235,13 @@ export function LoopForm({ loop }: LoopFormProps) {
223235
<LoopContextFields
224236
value={values.contextTarget}
225237
disabled={isSubmitting}
226-
onChange={(contextTarget) => patch({ contextTarget })}
238+
onChange={(contextTarget) =>
239+
patch(
240+
contextTarget
241+
? { contextTarget, visibility: "team" }
242+
: { contextTarget },
243+
)
244+
}
227245
/>
228246
</Field>
229247

packages/ui/src/features/loops/components/LoopsListView.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { navigateToNewLoop } from "@posthog/ui/router/navigationBridge";
1414
import { openUrlInBrowser } from "@posthog/ui/utils/browser";
1515
import { Flex, Heading, IconButton, Text } from "@radix-ui/themes";
1616
import { useMemo, useState } from "react";
17+
import { useLoopBuilderTask } from "../hooks/useLoopBuilderTask";
1718
import { useLoops } from "../hooks/useLoops";
1819
import { useLoopDraftStore } from "../loopDraftStore";
1920
import {
@@ -38,6 +39,8 @@ export function LoopsListView() {
3839
const [prompt, setPrompt] = useState("");
3940
const [templateCategory, setTemplateCategory] =
4041
useState<LoopTemplateCategory>("engineering");
42+
const { runTask: runLoopBuilder, isRunning: isBuildingLoop } =
43+
useLoopBuilderTask();
4144

4245
const headerContent = useMemo(
4346
() => (
@@ -59,9 +62,8 @@ export function LoopsListView() {
5962

6063
const startFromPrompt = () => {
6164
const text = prompt.trim();
62-
if (!text) return;
63-
useLoopDraftStore.getState().setPrefill({ instructions: text });
64-
navigateToNewLoop();
65+
if (!text || isBuildingLoop) return;
66+
void runLoopBuilder(text);
6567
};
6668

6769
const startBlank = () => {
@@ -185,8 +187,9 @@ export function LoopsListView() {
185187
<textarea
186188
value={prompt}
187189
rows={2}
190+
disabled={isBuildingLoop}
188191
placeholder="What do you want automated?"
189-
className="w-full resize-none bg-transparent text-[13px] text-gray-12 leading-relaxed outline-none placeholder:text-gray-9"
192+
className="w-full resize-none bg-transparent text-[13px] text-gray-12 leading-relaxed outline-none placeholder:text-gray-9 disabled:opacity-60"
190193
onChange={(e) => setPrompt(e.target.value)}
191194
onKeyDown={(e) => {
192195
if (e.key === "Enter" && !e.shiftKey) {
@@ -197,13 +200,15 @@ export function LoopsListView() {
197200
/>
198201
<Flex align="center" justify="between" gap="3">
199202
<Text className="text-[11px] text-gray-9">
200-
Drafts a loop you can review before it runs
203+
An agent builds the loop with you, then creates it on your
204+
confirmation
201205
</Text>
202206
<IconButton
203207
variant="solid"
204208
size="1"
205-
aria-label="Draft loop"
206-
disabled={!prompt.trim()}
209+
aria-label="Build loop with an agent"
210+
loading={isBuildingLoop}
211+
disabled={!prompt.trim() || isBuildingLoop}
207212
onClick={startFromPrompt}
208213
>
209214
<ArrowUpIcon size={13} weight="bold" />
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { TaskCreationInput } from "@posthog/core/task-detail/taskService";
2+
import {
3+
type InboxCloudTaskInputContext,
4+
useInboxCloudTaskRunner,
5+
} from "@posthog/ui/features/inbox/hooks/useInboxCloudTaskRunner";
6+
import { useUserRepositoryIntegration } from "@posthog/ui/features/integrations/useIntegrations";
7+
import {
8+
resolveDefaultCloudRepository,
9+
useSettingsStore,
10+
} from "@posthog/ui/features/settings/settingsStore";
11+
import { useCallback, useMemo, useRef } from "react";
12+
import { buildLoopBuilderPrompt } from "../loopBuilderPrompt";
13+
14+
interface UseLoopBuilderTaskReturn {
15+
/** Start an auto-mode cloud session that builds a loop from `instructions` and navigate to it. */
16+
runTask: (instructions: string) => Promise<void>;
17+
/** True while the session is being created. */
18+
isRunning: boolean;
19+
}
20+
21+
/**
22+
* The loops prompt box: start a cloud sandbox agent whose job is to build a Loop
23+
* with the user (ask clarifying questions, confirm, then create it via the PostHog
24+
* MCP `loops-create` tool). Mirrors `useScoutChatTask` — a repo-less, auto-mode
25+
* cloud task seeded with a canned instruction prompt. The user's typed text rides
26+
* in through a ref so the fixed `buildInput` closure reads the latest submission.
27+
*/
28+
export function useLoopBuilderTask(): UseLoopBuilderTaskReturn {
29+
const instructionsRef = useRef("");
30+
const { repositories } = useUserRepositoryIntegration();
31+
const lastUsedCloudRepository = useSettingsStore(
32+
(state) => state.lastUsedCloudRepository,
33+
);
34+
35+
const cloudRepository = useMemo(
36+
() => resolveDefaultCloudRepository(repositories, lastUsedCloudRepository),
37+
[lastUsedCloudRepository, repositories],
38+
);
39+
40+
const buildInput = useCallback(
41+
(ctx: InboxCloudTaskInputContext): TaskCreationInput => {
42+
const prompt = buildLoopBuilderPrompt({
43+
instructions: instructionsRef.current,
44+
});
45+
return {
46+
content: prompt,
47+
taskDescription: "Create a loop",
48+
// Building a loop is pure PostHog-MCP work; it runs repo-less when no
49+
// personal repo resolves, and passes one through harmlessly when it does.
50+
repository: ctx.cloudRepository,
51+
githubUserIntegrationId: ctx.githubUserIntegrationId ?? undefined,
52+
workspaceMode: "cloud",
53+
executionMode: "auto",
54+
adapter: ctx.adapter,
55+
model: ctx.model,
56+
reasoningLevel: ctx.reasoningLevel,
57+
};
58+
},
59+
[],
60+
);
61+
62+
const copy = useMemo(
63+
() => ({
64+
loadingTitle: "Starting loop builder...",
65+
errorTitle: "Failed to start loop builder",
66+
missingRepository: "Connect a GitHub repository before building a loop",
67+
missingIntegration: "Connect a GitHub integration to build a loop",
68+
signedOut: "Sign in to build a loop",
69+
missingModel:
70+
"Couldn't resolve a default model. Open a task once and pick a model, then try again.",
71+
}),
72+
[],
73+
);
74+
75+
const { run, isRunning } = useInboxCloudTaskRunner({
76+
cloudRepository,
77+
// Loop creation is pure PostHog-MCP work; a missing repo must not block it.
78+
allowMissingRepository: true,
79+
loggerScope: "loop-builder",
80+
copy,
81+
buildInput,
82+
});
83+
84+
const runTask = useCallback(
85+
async (instructions: string) => {
86+
instructionsRef.current = instructions;
87+
await run();
88+
},
89+
[run],
90+
);
91+
92+
return { runTask, isRunning };
93+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* The canned first-message the loop-builder cloud task starts with — the agent's
3+
* "custom instructions" for a session whose whole job is to create a Loop with the
4+
* user and then create it via the PostHog MCP `loops-create` tool. Mirrors the scout
5+
* authoring prompt (`packages/core/src/scouts/scoutPrompts.ts`).
6+
*/
7+
export function buildLoopBuilderPrompt({
8+
instructions,
9+
}: {
10+
instructions?: string;
11+
}): string {
12+
const seed = instructions?.trim();
13+
14+
return `Your job in this session is to help me create a Loop for this PostHog project, then create it for me.
15+
16+
A Loop is a named, cloud-executed agent automation: instructions the agent runs whenever a trigger fires (a schedule, a GitHub event, or an API call). Loops run unattended in a sandbox and can post results, open pull requests, and keep a context up to date.
17+
18+
${
19+
seed
20+
? `Here's what I want automated:\n\n${seed}\n`
21+
: `Start by asking me what I want automated, and offer a couple of concrete ideas.\n`
22+
}
23+
Walk me through building it:
24+
25+
1. Turn what I want into a clear set of loop instructions — the prompt the loop runs on every fire. Draft it and refine it with me.
26+
2. Ask me the essentials one focused question at a time (use your question tool so I can pick from options), keeping sensible defaults and inferring what you reasonably can rather than over-asking:
27+
- When it runs: a schedule (e.g. weekday mornings), on a GitHub event, or only when I trigger it manually.
28+
- Whether it works on a repository (for code changes and PRs) or is report-only.
29+
- Whether it may open pull requests, and how I want to hear about runs (in-app, email, or Slack).
30+
- A short name.
31+
3. When you have enough, show me a clear summary of the loop — name, what it does, when it runs, where it works, notifications — and ask me to confirm before creating anything.
32+
4. Only after I confirm, create it by calling the PostHog MCP \`loops-create\` tool with the assembled configuration. Make it a personal loop unless I ask otherwise. Then tell me it's created and where to find it.
33+
34+
Use the PostHog MCP loop tools: \`loops-list\` first to see what already exists so you don't duplicate one, and \`loops-create\` to create it. Do not create the loop until I've confirmed the summary.`;
35+
}

packages/ui/src/features/loops/loopFormTypes.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ export function emptyLoopFormValues(): LoopFormValues {
115115
};
116116
}
117117

118+
/** A context-attached loop files its runs into the context's shared feed, so it must be
119+
* team-visible. The backend rejects personal + context; this keeps form state consistent
120+
* for prefills (e.g. "New loop" from a context page) and legacy loops. */
121+
export function normalizeLoopFormValues(values: LoopFormValues): LoopFormValues {
122+
if (values.contextTarget && values.visibility !== "team") {
123+
return { ...values, visibility: "team" };
124+
}
125+
return values;
126+
}
127+
118128
export function loopToFormValues(loop: LoopSchemas.Loop): LoopFormValues {
119129
return {
120130
name: loop.name,
@@ -178,6 +188,9 @@ export function isLoopFormValid(values: LoopFormValues): boolean {
178188
if (!values.name.trim() || !values.instructions.trim()) {
179189
return false;
180190
}
191+
if (values.contextTarget && values.visibility !== "team") {
192+
return false;
193+
}
181194
return values.triggers.every((trigger) => isTriggerDraftValid(trigger));
182195
}
183196

0 commit comments

Comments
 (0)