-
{detail.session.name ?? `Session #${detail.session.id}`}
-
+
+
+
{detail.session.status} · {formatDuration(detail.session.durationMs)}
{detail.session.kind === "replay" && detail.replayedFromName !== undefined && (
<> · replayed from {detail.replayedFromName ?? `session #${detail.session.replayedSessionId}`}>
@@ -49,7 +50,7 @@ export default async function SessionDetailPage({ params }: { params: Promise<{
)}
{detail.session.description && (
-
{detail.session.description}
+
{detail.session.description}
)}
{canReplay && (
diff --git a/components/sessions/SessionTitleEditor.tsx b/components/sessions/SessionTitleEditor.tsx
new file mode 100644
index 0000000..7830876
--- /dev/null
+++ b/components/sessions/SessionTitleEditor.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/navigation";
+import { Check, Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+
+const AUTOSAVE_DELAY_MS = 600;
+
+export function SessionTitleEditor({ sessionId, initialName }: { sessionId: number; initialName: string | null }) {
+ const router = useRouter();
+ const initial = initialName ?? "";
+ const [value, setValue] = useState(initial);
+ const [status, setStatus] = useState<"idle" | "saving" | "saved">("idle");
+ const lastSaved = useRef(initial);
+
+ useEffect(() => {
+ const trimmed = value.trim();
+ if (trimmed.length === 0 || trimmed === lastSaved.current) return;
+
+ setStatus("saving");
+ const timer = setTimeout(async () => {
+ const res = await fetch(`/api/sessions/${sessionId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ name: trimmed }),
+ });
+ if (res.ok) {
+ lastSaved.current = trimmed;
+ setStatus("saved");
+ router.refresh();
+ } else {
+ setStatus("idle");
+ toast.error("Could not rename session");
+ }
+ }, AUTOSAVE_DELAY_MS);
+
+ return () => clearTimeout(timer);
+ }, [value, sessionId, router]);
+
+ useEffect(() => {
+ if (status !== "saved") return;
+ const timer = setTimeout(() => setStatus("idle"), 1500);
+ return () => clearTimeout(timer);
+ }, [status]);
+
+ return (
+
+
setValue(e.target.value)}
+ placeholder={`Session #${sessionId}`}
+ aria-label="Session name"
+ className="min-w-0 flex-1 rounded-md border border-transparent bg-transparent px-2 py-0.5 font-heading text-2xl font-semibold text-foreground outline-none transition-colors placeholder:text-muted-foreground/70 hover:border-border focus:border-border focus:bg-muted/30"
+ />
+
+
+
+
+
+ );
+}