666db0b939
Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval UX in chat, and add admin inspection routes with local verify scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
import { useState } from 'react';
|
||
import { cancelGoalRun, type GoalRunAwaitingItem } from '../api/goalRun';
|
||
|
||
type GoalRunAwaitingBannerProps = {
|
||
items: GoalRunAwaitingItem[];
|
||
busy: boolean;
|
||
onContinue: (input: {
|
||
goalRunId: string;
|
||
checkpointId: string;
|
||
feedback?: string | null;
|
||
}) => Promise<void>;
|
||
onDismiss: (goalRunId: string) => void;
|
||
onRefresh: () => Promise<void>;
|
||
};
|
||
|
||
function GoalRunAwaitingCard({
|
||
item,
|
||
busy,
|
||
onContinue,
|
||
onDismiss,
|
||
onRefresh,
|
||
}: {
|
||
item: GoalRunAwaitingItem;
|
||
busy: boolean;
|
||
onContinue: GoalRunAwaitingBannerProps['onContinue'];
|
||
onDismiss: (goalRunId: string) => void;
|
||
onRefresh: () => Promise<void>;
|
||
}) {
|
||
const [feedback, setFeedback] = useState('');
|
||
const [action, setAction] = useState<'continue' | 'cancel' | null>(null);
|
||
const [localError, setLocalError] = useState<string | null>(null);
|
||
const { goal, checkpoint } = item;
|
||
const summary = checkpoint.outputSummary?.trim();
|
||
|
||
const handleContinue = async () => {
|
||
setLocalError(null);
|
||
setAction('continue');
|
||
try {
|
||
await onContinue({
|
||
goalRunId: goal.id,
|
||
checkpointId: checkpoint.id,
|
||
feedback: feedback.trim() || null,
|
||
});
|
||
onDismiss(goal.id);
|
||
} catch (err) {
|
||
setLocalError(err instanceof Error ? err.message : String(err));
|
||
await onRefresh();
|
||
} finally {
|
||
setAction(null);
|
||
}
|
||
};
|
||
|
||
const handleCancel = async () => {
|
||
setLocalError(null);
|
||
setAction('cancel');
|
||
try {
|
||
await cancelGoalRun(goal.id);
|
||
onDismiss(goal.id);
|
||
} catch (err) {
|
||
setLocalError(err instanceof Error ? err.message : String(err));
|
||
await onRefresh();
|
||
} finally {
|
||
setAction(null);
|
||
}
|
||
};
|
||
|
||
const cardBusy = busy || action != null;
|
||
|
||
return (
|
||
<div className="banner banner-info goal-run-awaiting-banner">
|
||
<div className="goal-run-awaiting-banner-body">
|
||
<strong>{goal.title}</strong>
|
||
<span>
|
||
阶段「{checkpoint.title}」已完成,请确认后继续下一阶段。
|
||
</span>
|
||
{summary ? <span className="goal-run-awaiting-summary">{summary}</span> : null}
|
||
<textarea
|
||
className="goal-run-awaiting-feedback"
|
||
rows={2}
|
||
placeholder="可选:补充反馈或修改方向"
|
||
value={feedback}
|
||
disabled={cardBusy}
|
||
onChange={(event) => setFeedback(event.target.value)}
|
||
/>
|
||
{localError ? <span className="goal-run-awaiting-error">{localError}</span> : null}
|
||
</div>
|
||
<div className="goal-run-awaiting-actions">
|
||
<button
|
||
type="button"
|
||
className="banner-action"
|
||
disabled={cardBusy}
|
||
onClick={() => void handleContinue()}
|
||
>
|
||
{action === 'continue' ? '处理中…' : '确认并继续'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="banner-dismiss"
|
||
disabled={cardBusy}
|
||
onClick={() => void handleCancel()}
|
||
>
|
||
{action === 'cancel' ? '取消中…' : '取消目标'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function GoalRunAwaitingBanner({
|
||
items,
|
||
busy,
|
||
onContinue,
|
||
onDismiss,
|
||
onRefresh,
|
||
}: GoalRunAwaitingBannerProps) {
|
||
if (!items.length) return null;
|
||
|
||
return (
|
||
<div className="goal-run-awaiting-stack">
|
||
{items.map((item) => (
|
||
<GoalRunAwaitingCard
|
||
key={item.goal.id}
|
||
item={item}
|
||
busy={busy}
|
||
onContinue={onContinue}
|
||
onDismiss={onDismiss}
|
||
onRefresh={onRefresh}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|