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.
169 lines
5.3 KiB
TypeScript
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;
|
|
}
|