feat(deploy): structured execution-confirm card replaces raw JSON dump

Sprint 4. Closes M6 from the product-doc gap analysis.

Previously the "Review" tab of the new-deployment sheet showed a
JSON.stringify of the full orchestration plan + a single checkbox.
Two problems:
  1. The raw JSON is nearly unreadable to non-engineering users
     (the typical operator of this surface).
  2. Product docs §10 "执行前确认卡" require five named sections,
     not a freeform payload dump:
       - 本次会做 (this run will do)
       - 本次允许使用 (resources this run may use)
       - 本次不会做 (this run will NOT do — static off-limits list)
       - 高危规则 (high-risk policy reminder)
       - 预计消耗 (token / cost / duration triple)

New layout puts each section in its own card pulled directly from
previewPlan so what the user confirms is byte-identical to what
gets submitted. The "本次不会做" card is static red-bordered text:
production deploys, prod DB writes, long-lived credential
extraction, any resource not listed.

The raw JSON is preserved behind a <details> disclosure so power
users debugging the payload still have access. Same checkbox
behaviour gates submit — no policy regression.

All strings localized en + zh.

Verification:
  - tsc --noEmit clean
  - data binds directly to previewPlan — no drift risk between
    preview and actual submit
  - no backend change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 20:25:22 +08:00
co-authored by Claude Opus 4.7
parent 4bab918808
commit 131639f672
3 changed files with 207 additions and 7 deletions
@@ -1196,14 +1196,172 @@ export function CreateAgnetDeploymentSheet({
<TabsContent value='review' className='mt-0 space-y-4'>
<SectionTitle
icon={ShieldCheck}
title={t('Permissions and manifest')}
title={t('Confirm before launch')}
hint={t(
'Review the runtime model and resource grants before submitting.'
'Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.'
)}
/>
<pre className='max-h-[420px] overflow-auto rounded-lg border bg-muted/40 p-3 font-mono text-[11px] leading-relaxed'>
{/* M6 execution-confirm card — five sections per product docs
§10. Replaces the raw JSON dump that overwhelmed users
while still leaving the JSON behind a disclosure for
power users. Data is derived 1:1 from previewPlan so it
always matches what gets submitted. */}
<div className='space-y-3'>
{/* 1. 本次会做 — Objective + role list */}
<div className='rounded-lg border bg-card/40 p-3'>
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
{t('This run will do')}
</p>
<p className='mt-2 text-sm text-foreground'>
{objective.trim() || (
<span className='text-muted-foreground italic'>
{t('No objective provided yet — go back to the Idea tab.')}
</span>
)}
</p>
{previewPlan.agent_runtime.agents.length > 0 && (
<div className='mt-2 flex flex-wrap gap-1'>
{previewPlan.agent_runtime.agents.map((a) => (
<span
key={a.role}
className='inline-flex items-center gap-1 rounded-md bg-[color-mix(in_oklch,var(--primary)_12%,transparent)] px-1.5 py-0.5 font-mono text-[10px] text-primary'
>
{a.role}
<span className='text-muted-foreground'>·</span>
{a.model_ref}
</span>
))}
</div>
)}
</div>
{/* 2. 本次允许使用 — Resources from permission_manifest */}
<div className='rounded-lg border bg-card/40 p-3'>
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
{t('Resources this run may use')}
</p>
{previewPlan.permission_manifest.resource_grants.length === 0 ? (
<p className='mt-2 text-sm text-muted-foreground italic'>
{t('No resources bound yet — Agnet will run with no external data access.')}
</p>
) : (
<ul className='mt-2 space-y-1 text-xs'>
{previewPlan.permission_manifest.resource_grants.map(
(g, i) => (
<li
key={
(g as { grant_id?: string }).grant_id ||
`${(g as { resource_type?: string }).resource_type}-${i}`
}
className='flex items-center gap-2'
>
<span className='inline-flex items-center rounded bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-1 font-mono text-[10px] text-primary'>
{(g as { resource_type?: string }).resource_type ||
'?'}
</span>
<span className='font-mono text-[11px] text-muted-foreground'>
{(g as { resource_id?: string }).resource_id || '—'}
</span>
<span className='text-muted-foreground'>·</span>
<span className='text-[11px]'>
{Array.isArray(
(g as { permission_scope?: string[] })
.permission_scope
) &&
((g as { permission_scope?: string[] })
.permission_scope as string[]).length > 0
? ((g as { permission_scope?: string[] })
.permission_scope as string[]).join(', ')
: t('no actions specified')}
</span>
</li>
)
)}
</ul>
)}
</div>
{/* 3. 本次不会做 — Static off-limits list per §10 */}
<div className='rounded-lg border border-dashed border-rose-500/30 bg-rose-500/5 p-3'>
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-rose-400'>
{t('This run will NOT do')}
</p>
<ul className='mt-2 grid gap-1 text-xs text-muted-foreground sm:grid-cols-2'>
<li>· {t('Production deploys without client approval')}</li>
<li>· {t('Production database writes')}</li>
<li>· {t('Long-lived credential extraction')}</li>
<li>· {t('Any resource not listed above')}</li>
</ul>
</div>
{/* 4. 高危规则 — Risk level + escalation policy */}
<div className='rounded-lg border bg-card/40 p-3'>
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
{t('High-risk operations')}
</p>
<p className='mt-2 text-xs text-foreground'>
<span
className={cn(
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ring-1 ring-inset',
riskLevel === 'high'
? 'bg-rose-500/15 text-rose-400 ring-rose-500/30'
: riskLevel === 'medium'
? 'bg-amber-500/15 text-amber-400 ring-amber-500/30'
: 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30'
)}
>
{t('Risk level')}: {riskLevel}
</span>
</p>
<p className='mt-2 text-xs leading-relaxed text-muted-foreground'>
{t(
'Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.'
)}
</p>
</div>
{/* 5. 预计消耗 — Budget triple */}
<div className='rounded-lg border bg-card/40 p-3'>
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
{t('Expected consumption')}
</p>
<div className='mt-2 grid gap-2 text-xs sm:grid-cols-3'>
<div>
<p className='text-muted-foreground'>{t('Max tokens')}</p>
<p className='font-mono text-sm text-foreground'>
{maxTokens.toLocaleString()}
</p>
</div>
<div>
<p className='text-muted-foreground'>{t('Max cost')}</p>
<p className='font-mono text-sm text-foreground'>
${maxCost}
</p>
</div>
<div>
<p className='text-muted-foreground'>
{t('Max duration')}
</p>
<p className='font-mono text-sm text-foreground'>
{Math.round(maxDurationSec / 60)} {t('min')}
</p>
</div>
</div>
</div>
</div>
{/* Power-user disclosure: raw payload. Folded by default so
the structured five-section card stays the primary view. */}
<details className='rounded-lg border bg-muted/20 p-2 text-xs'>
<summary className='cursor-pointer select-none text-muted-foreground'>
{t('Show raw payload (advanced)')}
</summary>
<pre className='mt-2 max-h-[300px] overflow-auto rounded bg-background/60 p-3 font-mono text-[10px] leading-relaxed'>
{JSON.stringify(previewPlan, null, 2)}
</pre>
</details>
<label className='flex items-start gap-2 rounded-lg border p-3 text-sm'>
<Checkbox
checked={permissionConfirmed}
+22 -1
View File
@@ -4078,6 +4078,27 @@
"Allowed actions": "Allowed actions",
"Constraints": "Constraints",
"secret_ref": "secret_ref",
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself."
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.",
"Confirm before launch": "Confirm before launch",
"Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.": "Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.",
"This run will do": "This run will do",
"No objective provided yet — go back to the Idea tab.": "No objective provided yet — go back to the Idea tab.",
"Resources this run may use": "Resources this run may use",
"No resources bound yet — Agnet will run with no external data access.": "No resources bound yet — Agnet will run with no external data access.",
"no actions specified": "no actions specified",
"This run will NOT do": "This run will NOT do",
"Production deploys without client approval": "Production deploys without client approval",
"Production database writes": "Production database writes",
"Long-lived credential extraction": "Long-lived credential extraction",
"Any resource not listed above": "Any resource not listed above",
"High-risk operations": "High-risk operations",
"Risk level": "Risk level",
"Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.": "Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.",
"Expected consumption": "Expected consumption",
"Max tokens": "Max tokens",
"Max cost": "Max cost",
"Max duration": "Max duration",
"min": "min",
"Show raw payload (advanced)": "Show raw payload (advanced)"
}
}
+22 -1
View File
@@ -4078,6 +4078,27 @@
"Allowed actions": "允许动作",
"Constraints": "约束",
"secret_ref": "密钥引用",
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "永远不展示明文密钥。表中的 secret_ref 只是密钥保管器里的引用指针,不是密钥本体。"
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "永远不展示明文密钥。表中的 secret_ref 只是密钥保管器里的引用指针,不是密钥本体。",
"Confirm before launch": "确认后启动",
"Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.": "Heicode 已汇总本次会做什么、能用哪些资源、哪些事情不会做、以及预计消耗。请确认后启动。",
"This run will do": "本次会做",
"No objective provided yet — go back to the Idea tab.": "还没填写目标 — 请回到「想法」标签页补充。",
"Resources this run may use": "本次允许使用的资源",
"No resources bound yet — Agnet will run with no external data access.": "还没绑定资源 — Agnet 将在无外部数据访问的情况下运行。",
"no actions specified": "未指定动作",
"This run will NOT do": "本次不会做",
"Production deploys without client approval": "未经客户端审批的生产部署",
"Production database writes": "生产数据库写入",
"Long-lived credential extraction": "导出长期密钥",
"Any resource not listed above": "未在上方列出的任何资源",
"High-risk operations": "高危操作",
"Risk level": "风险等级",
"Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.": "任何高危操作(生产部署、密钥访问、破坏性改动)都必须经过桌面客户端的明确审批。审批通过后才下发短期、最小权限的临时凭证,全程记入审计。",
"Expected consumption": "预计消耗",
"Max tokens": "Token 上限",
"Max cost": "费用上限",
"Max duration": "时长上限",
"min": "分钟",
"Show raw payload (advanced)": "查看原始负载(高级)"
}
}