Plan actions post to the decision API and reconcile on 409 instead of mutating documents. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
import { CardActionStatus, InteractiveCardData } from '../types/workbench';
|
||
import type { IntentPlanView } from '../api/intentPlans';
|
||
|
||
export function isPlanConfirmCard(
|
||
card?: Pick<InteractiveCardData, 'cardType' | 'planId'> | null,
|
||
payload?: Record<string, string>,
|
||
): boolean {
|
||
if (card?.cardType === 'PLAN_CONFIRM' || card?.planId) return true;
|
||
return Boolean(payload?.plan_id);
|
||
}
|
||
|
||
export function planCardTouchesLocalAST(_actionId?: string): boolean {
|
||
return false;
|
||
}
|
||
|
||
export function mapPlanStatusToCard(status: IntentPlanView['status']): CardActionStatus {
|
||
switch (status) {
|
||
case 'executing':
|
||
return 'executing';
|
||
case 'completed':
|
||
case 'partially_completed':
|
||
return 'resolved';
|
||
case 'cancelled':
|
||
case 'superseded':
|
||
return 'cancelled';
|
||
case 'expired':
|
||
return 'expired';
|
||
case 'failed':
|
||
return 'outdated';
|
||
default:
|
||
return 'active';
|
||
}
|
||
}
|
||
|
||
export function cardFromPlanView(plan: IntentPlanView): InteractiveCardData {
|
||
const steps = plan.steps ?? [];
|
||
const writeish = steps.filter((s) => s.action_key.startsWith('task.') || s.action_key.includes('run'));
|
||
const summary =
|
||
plan.safe_summary ||
|
||
steps.map((s) => s.safe_summary || s.action_key).filter(Boolean).join(';');
|
||
return {
|
||
cardId: `plan-${plan.plan_id}-r${plan.revision}`,
|
||
cardType: 'PLAN_CONFIRM',
|
||
title: `计划确认(${steps.length} 步)`,
|
||
proposedContent: summary,
|
||
impactRisk:
|
||
writeish.length > 0
|
||
? '含生成/写入:逐步提交,后续失败不回滚'
|
||
: '只读查询:确认后按步执行',
|
||
status: mapPlanStatusToCard(plan.status),
|
||
ttlTurns: 3,
|
||
planId: plan.plan_id,
|
||
planRevision: plan.revision,
|
||
stepCount: steps.length,
|
||
validatedOnly: plan.execution_mode === 'validated_only',
|
||
actions: [
|
||
{
|
||
actionId: 'plan_confirm',
|
||
label: '确认执行',
|
||
style: 'primary',
|
||
isAffirmativeDefault: true,
|
||
payload: {
|
||
plan_id: plan.plan_id,
|
||
expected_revision: String(plan.revision),
|
||
kind: 'confirm',
|
||
},
|
||
},
|
||
{
|
||
actionId: 'plan_cancel',
|
||
label: '取消计划',
|
||
style: 'subtle',
|
||
payload: {
|
||
plan_id: plan.plan_id,
|
||
expected_revision: String(plan.revision),
|
||
kind: 'cancel',
|
||
},
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
export function applyPlanViewToCard(card: InteractiveCardData, plan: IntentPlanView): InteractiveCardData {
|
||
const next = cardFromPlanView(plan);
|
||
return {
|
||
...card,
|
||
...next,
|
||
cardId: card.cardId,
|
||
};
|
||
}
|
||
|
||
export function markPlanCard(card: InteractiveCardData, status: CardActionStatus, summary?: string): InteractiveCardData {
|
||
return {
|
||
...card,
|
||
status,
|
||
resolutionSummary: summary ?? card.resolutionSummary,
|
||
};
|
||
}
|
||
|
||
export function reconcileConflictCard(card: InteractiveCardData, latest?: IntentPlanView | null): InteractiveCardData {
|
||
if (latest) {
|
||
return { ...applyPlanViewToCard(card, latest), status: 'outdated' };
|
||
}
|
||
return markPlanCard(card, 'outdated', '计划已更新,请查看最新版本');
|
||
}
|