diff --git a/packages/ui/src/features/loops/components/LoopDetailView.tsx b/packages/ui/src/features/loops/components/LoopDetailView.tsx
index ff8685713f..e1ed412ef5 100644
--- a/packages/ui/src/features/loops/components/LoopDetailView.tsx
+++ b/packages/ui/src/features/loops/components/LoopDetailView.tsx
@@ -1,4 +1,8 @@
-import { ArrowLeftIcon, RepeatIcon } from "@phosphor-icons/react";
+import {
+ ArrowLeftIcon,
+ PencilSimpleIcon,
+ RepeatIcon,
+} from "@phosphor-icons/react";
import type { LoopSchemas } from "@posthog/api-client/loops";
import { isUploadableSkillSource } from "@posthog/core/message-editor/skillTags";
import { useHostTRPC } from "@posthog/host-router/react";
@@ -32,9 +36,10 @@ import {
} from "@posthog/ui/router/navigationBridge";
import { track } from "@posthog/ui/shell/analytics";
import { useHostCapabilities } from "@posthog/ui/shell/useHostCapabilities";
-import { Flex, Text } from "@radix-ui/themes";
+import { Flex, Text, TextField } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
+import { useInlineEdit } from "../hooks/useInlineEdit";
import { useLoop } from "../hooks/useLoop";
import {
useDeleteLoop,
@@ -215,10 +220,8 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
-
-
- {loop.name}
-
+
+
{loopStatusLabel(loop)}
@@ -257,11 +260,7 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
- {loop.description.trim() ? (
-
- {loop.description}
-
- ) : null}
+
@@ -347,6 +346,123 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
);
}
+function EditableLoopTitle({ loop }: { loop: LoopSchemas.Loop }) {
+ const updateLoop = useUpdateLoop(loop.id);
+ const edit = useInlineEdit({
+ current: loop.name,
+ isPending: updateLoop.isPending,
+ commitOnEnter: "enter",
+ onCommit: (name, { reset }) =>
+ updateLoop.mutate(
+ { name },
+ {
+ onSuccess: () => {
+ reset();
+ toast.success("Loop title updated");
+ },
+ onError: (error) => {
+ reset();
+ toast.error("Failed to update loop title", {
+ description: error.message,
+ });
+ },
+ },
+ ),
+ });
+
+ if (!edit.isEditing) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+function EditableLoopDescription({ loop }: { loop: LoopSchemas.Loop }) {
+ const updateLoop = useUpdateLoop(loop.id);
+ const edit = useInlineEdit({
+ current: loop.description,
+ isPending: updateLoop.isPending,
+ allowEmpty: true,
+ onCommit: (description, { reset }) =>
+ updateLoop.mutate(
+ { description },
+ {
+ onSuccess: () => {
+ reset();
+ toast.success("Loop description updated");
+ },
+ onError: (error) => {
+ reset();
+ toast.error("Failed to update loop description", {
+ description: error.message,
+ });
+ },
+ },
+ ),
+ });
+
+ if (!edit.isEditing) {
+ const hasDescription = loop.description.trim();
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
function loopStatusBadgeVariant(
loop: LoopSchemas.Loop,
): "default" | "destructive" | "success" {
@@ -563,41 +679,26 @@ function LoopSkillSummary({ loop }: { loop: LoopSchemas.Loop }) {
function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
const updateLoop = useUpdateLoop(loop.id);
const primarySkill = primaryLoopSkillBundle(loop);
- const [draft, setDraft] = useState(null);
- // Escape reverts and blurs; skip the resulting onBlur save.
- const skipCommit = useRef(false);
-
- const commit = (value: string) => {
- if (skipCommit.current) {
- skipCommit.current = false;
- return;
- }
- const trimmed = value.trim();
- if (!trimmed) {
- setDraft(null);
- return;
- }
- if (updateLoop.isPending) return;
- if (trimmed === loop.instructions.trim()) {
- setDraft(null);
- return;
- }
- updateLoop.mutate(
- { instructions: trimmed },
- {
- onSuccess: () => {
- setDraft(null);
- toast.success("Instructions updated");
- },
- onError: (error) => {
- setDraft(null);
- toast.error("Failed to update instructions", {
- description: error.message,
- });
+ const edit = useInlineEdit({
+ current: loop.instructions,
+ isPending: updateLoop.isPending,
+ onCommit: (instructions, { reset }) =>
+ updateLoop.mutate(
+ { instructions },
+ {
+ onSuccess: () => {
+ reset();
+ toast.success("Instructions updated");
+ },
+ onError: (error) => {
+ reset();
+ toast.error("Failed to update instructions", {
+ description: error.message,
+ });
+ },
},
- },
- );
- };
+ ),
+ });
return (
@@ -610,19 +711,11 @@ function InstructionsSection({ loop }: { loop: LoopSchemas.Loop }) {
) : null}