Files
Codewalkers/apps/web/src/components/editor/RefineAgentPanel.tsx
Lukas May f5b1a3a5b9 feat: pre-populate retry dialog with crashed agent's original instruction
When a refine agent crashes, the Retry dialog now extracts the
user_instruction from the agent's stored prompt and pre-fills the
textarea, so users can re-run with the same instruction without
retyping it.
2026-03-06 21:13:03 +01:00

169 lines
5.3 KiB
TypeScript

import { useCallback, useEffect, useMemo } from "react";
import { Loader2, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { QuestionForm } from "@/components/QuestionForm";
import { ChangeSetBanner } from "@/components/ChangeSetBanner";
import { RefineSpawnDialog } from "../RefineSpawnDialog";
import { useRefineAgent } from "@/hooks";
function extractInstruction(prompt: string | null | undefined): string | undefined {
if (!prompt) return undefined;
const match = prompt.match(/<user_instruction>\n([\s\S]*?)\n<\/user_instruction>/);
return match?.[1] || undefined;
}
interface RefineAgentPanelProps {
initiativeId: string;
}
export function RefineAgentPanel({ initiativeId }: RefineAgentPanelProps) {
// All agent logic is now encapsulated in the hook
const { state, agent, questions, changeSet, spawn, resume, stop, dismiss, refresh } = useRefineAgent(initiativeId);
// spawn.mutate and resume.mutate are stable (ref-backed in useRefineAgent),
// so these callbacks won't change on every render.
const handleSpawn = useCallback((instruction?: string) => {
spawn.mutate({
initiativeId,
instruction,
});
}, [initiativeId, spawn.mutate]);
const handleAnswerSubmit = useCallback(
(answers: Record<string, string>) => {
resume.mutate(answers);
},
[resume.mutate],
);
const handleDismiss = useCallback(() => {
dismiss();
}, [dismiss]);
// Cmd+Enter (Mac) / Ctrl+Enter (Windows) dismisses when completed
useEffect(() => {
if (state !== "completed") return;
const handler = (e: KeyboardEvent) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
handleDismiss();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [state, handleDismiss]);
// No active agent — show spawn button
if (state === "none") {
return (
<div className="mb-3">
<RefineSpawnDialog
triggerText="Refine with Agent"
title="Refine Initiative Content"
description="An agent will review all pages and suggest improvements. Optionally tell it what to focus on."
instructionPlaceholder="What should the agent focus on? (optional)"
isSpawning={spawn.isPending}
error={spawn.error?.message}
onSpawn={handleSpawn}
/>
</div>
);
}
// Running
if (state === "running") {
const runningLabel = agent?.mode === 'discuss' ? 'Architect is discussing...' : 'Architect is refining...';
return (
<div className="mb-3 flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2">
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
<span className="text-sm text-muted-foreground">
{runningLabel}
</span>
</div>
);
}
// Waiting for input — show inline questions
if (state === "waiting" && questions) {
return (
<div className="mb-3 rounded-lg border border-border bg-card p-4">
<h3 className="text-sm font-semibold mb-3">Agent has questions</h3>
<QuestionForm
questions={questions.questions}
onSubmit={handleAnswerSubmit}
onCancel={() => {
// Can't cancel mid-question — just dismiss
}}
onDismiss={() => stop.mutate()}
isSubmitting={resume.isPending}
isDismissing={stop.isPending}
/>
</div>
);
}
// Completed with change set
if (state === "completed" && changeSet) {
return (
<div className="mb-3">
<ChangeSetBanner
changeSet={changeSet}
onDismiss={handleDismiss}
/>
</div>
);
}
// Completed without changes
if (state === "completed") {
return (
<div className="mb-3 flex items-center gap-2 rounded-lg border border-border bg-card px-3 py-2">
<span className="text-sm text-muted-foreground">
Agent completed no changes made.
</span>
<Button variant="ghost" size="sm" onClick={handleDismiss}>
Dismiss
</Button>
</div>
);
}
// Crashed
const crashedInstruction = useMemo(
() => (state === "crashed" ? extractInstruction(agent?.prompt) : undefined),
[state, agent?.prompt],
);
if (state === "crashed") {
return (
<div className="mb-3 rounded-lg border border-destructive/50 bg-destructive/5 px-3 py-2">
<div className="flex items-center gap-2">
<AlertCircle className="h-3.5 w-3.5 text-destructive" />
<span className="text-sm text-destructive">Agent crashed</span>
<RefineSpawnDialog
triggerText="Retry"
title="Refine Initiative Content"
description="An agent will review all pages and suggest improvements."
instructionPlaceholder="What should the agent focus on? (optional)"
defaultInstruction={crashedInstruction}
isSpawning={spawn.isPending}
error={spawn.error?.message}
onSpawn={handleSpawn}
trigger={
<Button
variant="outline"
size="sm"
className="ml-auto"
>
Retry
</Button>
}
/>
</div>
</div>
);
}
return null;
}