Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ export const typedActionProposalSchema = z.object({
gate: z.record(z.string(), z.unknown()).nullable().optional(),
error: z.record(z.string(), z.unknown()).nullable().optional(),
checkpoint: z.record(z.string(), z.unknown()).nullable().optional(),
failure: z.record(z.string(), z.unknown()).nullable().optional(),
canonical_update_basis: z.object({
schema_version: z.literal("loopx_chat_canonical_update_basis_v0"),
provider_revision: z.string().min(1),
source_authority: z.enum(["file_v0", "sqlite_v0"]),
registry_sha256: z.string().regex(/^[a-f0-9]{64}$/),
}).optional(),
regenerated_from: z.string().nullable().optional(),
operation: typedOperationEnvelopeSchema.nullable().optional(),
created_at: z.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
: selection.kind === "run" ? t("drawer.runDetails")
: selection.kind === "output" ? t("drawer.titleOutput")
: selection.kind === "proposal" && selection.item.actionKind === "team.plan" && selection.item.status === "applied" ? t("proposal.teamPlan.resultTitle")
: selection.kind === "proposal" ? t(selection.item.status === "applied" ? "drawer.titleProposalApplied" : "drawer.titleProposalConfirm")
: selection.kind === "proposal" ? t(selection.item.reviewPlan?.retryOriginal ? "drawer.recoverEditResult" : selection.item.status === "applied" ? "drawer.titleProposalApplied" : "drawer.titleProposalConfirm")
: selection.kind === "schedule" ? (selection.item.scheduleKind === "heartbeat" ? "Heartbeat" : t("drawer.titleSchedule"))
: t("drawer.goalDetails");
const goalId = selection.kind === "proposal" ? selection.item.goalId ?? "manager"
Expand Down Expand Up @@ -966,7 +966,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<small>{selection.item.actionKind} · {selection.item.status}</small>
<h3>{selection.item.title}</h3>
<p>{selection.item.impact}</p>
{selection.item.reviewPlan && selection.item.actionKind !== "team.plan" ? <p className="personal-proposal-explainer" data-action-review={selection.item.reviewPlan.interaction}>{selection.item.actionKind === "operation.execute" && selection.item.status === "gated"
{selection.item.reviewPlan && !selection.item.reviewPlan.retryOriginal && selection.item.actionKind !== "team.plan" ? <p className="personal-proposal-explainer" data-action-review={selection.item.reviewPlan.interaction}>{selection.item.actionKind === "operation.execute" && selection.item.status === "gated"
? t("actionReview.operation_group_confirmation")
: selection.item.actionKind === "operation.execute" && selection.item.reviewPlan.reason === "readback_unverified"
? t("actionReview.operation_result_delivery_pending")
Expand All @@ -977,7 +977,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
{selection.item.status === "applied" && selection.item.actionKind !== "team.plan" ? <p className={`personal-proposal-state ${selection.item.actionKind === "operation.execute" && selection.item.reviewPlan?.reason === "readback_unverified" ? "is-gated" : "is-applied"}`}><Check size={16} />{selection.item.actionKind === "operation.execute" ? selection.item.primaryLabel : t("drawer.proposalApplied")}</p> : null}
{selection.item.status === "applied" && selection.item.actionKind !== "operation.execute" && selection.item.goalId ? <button className="personal-primary-action" onClick={() => { const goalId = selection.item.goalId!; onClose(); void callbacks.onOpenGoal?.(goalId); }} type="button"><ExternalLink size={16} />{selection.item.actionKind === "goal.create" ? t("drawer.proposalEnterGoal") : t(selection.item.actionKind === "team.plan" ? "proposal.teamPlan.openGoal" : "drawer.proposalViewGoal")}</button> : null}
{selection.item.status === "stale" ? <p className="personal-proposal-state is-stale">{t("drawer.proposalStale")}</p> : null}
{selection.item.status === "error" ? <div className="personal-proposal-state is-error"><span>{selection.item.reviewPlan?.reason === "readback_unverified" ? t("actionReview.readback_unverified") : t("drawer.proposalApplyFailed")}</span>{selection.item.errorMessage ? <small>{selection.item.errorMessage}</small> : null}<small>{t(selection.item.actionKind === "team.plan" ? "proposal.teamPlan.retryHint" : "drawer.proposalApplyFailedHint")}</small></div> : null}
{selection.item.status === "error" && !selection.item.reviewPlan?.retryOriginal ? <div className="personal-proposal-state is-error"><span>{selection.item.reviewPlan?.reason === "readback_unverified" ? t("actionReview.readback_unverified") : t("drawer.proposalApplyFailed")}</span>{selection.item.errorMessage ? <small>{selection.item.errorMessage}</small> : null}<small>{t(selection.item.actionKind === "team.plan" ? "proposal.teamPlan.retryHint" : "drawer.proposalApplyFailedHint")}</small></div> : null}
{selection.item.status === "rejected" ? <p className="personal-proposal-state is-error">{t("drawer.proposalRejected")}</p> : null}
{selection.item.status === "deferred" ? <p className="personal-proposal-state is-gated">{t("drawer.proposalDeferred")}</p> : null}
{selection.item.status === "gated" ? <div className="personal-proposal-state is-gated"><span><strong>{selection.item.actionKind === "operation.execute" ? selection.item.primaryLabel : t("drawer.gateRequiresHost")}</strong>{selection.item.actionKind === "operation.execute" ? selection.item.impact : t("drawer.gateRequiresHostDescription")}</span>{selection.item.gate?.nextAction ? <small>{selection.item.gate.nextAction}</small> : null}</div> : null}
Expand All @@ -995,7 +995,7 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
);
})() : null}
{!readOnly && selection.item.workspaceCandidates?.length ? <div className="personal-workspace-candidates" aria-label={t("drawer.workspaceCandidates")}>{selection.item.workspaceCandidates.map((candidate) => <button key={candidate.workspaceRef} onClick={() => void callbacks.onSelectWorkspaceCandidate?.(selection.item, candidate.workspaceRef)} type="button"><strong>{candidate.label}</strong><small>{candidate.workspaceRef}</small></button>)}</div> : null}
{!readOnly && selection.item.actionKind !== "operation.execute" && selection.item.status === "error" ? <button className="personal-primary-action" onClick={() => void (selection.item.actionKind === "team.plan" ? callbacks.onApplyProposal?.(selection.item) : callbacks.onTransitionProposal?.(selection.item, "regenerate"))} type="button"><RotateCcw size={17} />{t(selection.item.actionKind === "team.plan" ? "proposal.teamPlan.retry" : "drawer.proposalRegenerate")}</button> : !readOnly && selection.item.actionKind !== "operation.execute" && selection.item.status !== "gated" && !(selection.item.actionKind === "team.plan" && selection.item.status === "applied") ? <button className="personal-primary-action" disabled={!['ready', 'deferred'].includes(selection.item.status) || selection.item.reviewPlan?.canApply === false} onClick={() => void callbacks.onApplyProposal?.(selection.item)} type="button"><Check size={17} />{selection.item.status === "applying" ? t("drawer.applying") : selection.item.primaryLabel ?? t("drawer.apply")}</button> : null}
{!readOnly && selection.item.actionKind !== "operation.execute" && selection.item.status === "error" ? <button className="personal-primary-action" onClick={() => void ((selection.item.actionKind === "team.plan" || selection.item.reviewPlan?.retryOriginal) ? callbacks.onApplyProposal?.(selection.item) : callbacks.onTransitionProposal?.(selection.item, "regenerate"))} type="button"><RotateCcw size={17} />{t(selection.item.reviewPlan?.retryOriginal ? "drawer.retryOriginal" : selection.item.actionKind === "team.plan" ? "proposal.teamPlan.retry" : "drawer.proposalRegenerate")}</button> : !readOnly && selection.item.actionKind !== "operation.execute" && selection.item.status !== "gated" && !(selection.item.actionKind === "team.plan" && selection.item.status === "applied") ? <button className="personal-primary-action" disabled={!['ready', 'deferred'].includes(selection.item.status) || selection.item.reviewPlan?.canApply === false} onClick={() => void callbacks.onApplyProposal?.(selection.item)} type="button"><Check size={17} />{selection.item.status === "applying" ? t("drawer.applying") : selection.item.primaryLabel ?? t("drawer.apply")}</button> : null}
{!readOnly && selection.item.actionKind !== "operation.execute" && (["stale", "gated", "rejected"].includes(selection.item.status) || (selection.item.status === "ready" && selection.item.reviewPlan?.canApply === false)) ? <button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "regenerate")} type="button"><RotateCcw size={16} />{t("drawer.proposalRecheck")}</button> : null}
{!readOnly && selection.item.actionKind !== "operation.execute" && ["ready", "gated"].includes(selection.item.status) ? <div className="personal-drawer-action-grid"><button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "defer")} type="button">{t("drawer.proposalDefer")}</button><button className="personal-secondary-action" onClick={() => void callbacks.onTransitionProposal?.(selection.item, "reject")} type="button">{t("drawer.decisionReject")}</button></div> : null}
{!["applied", "applying"].includes(selection.item.status) ? <button className="personal-secondary-action" onClick={onClose} type="button">{t("drawer.proposalClose")}</button> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,10 @@ const en = {
"actionReview.readback_unverified": "The action returned without verified readback. Completion is not confirmed; recheck the state.",
"actionReview.operation_group_confirmation": "This exact request can only be confirmed on its original card in the bound Feishu group. The Dashboard does not expose a local execution control.",
"actionReview.operation_result_delivery_pending": "The operation outcome was recorded, but the original group result card has not passed readback verification yet.",
"drawer.recoverEditResult": "Recover edit result",
"drawer.retryOriginal": "Retry original operation",
"actionReview.canonical_update_retry": "The edit is not yet verified. Retry this operation to recover its result.",
"actionReview.canonical_update_projection_pending": "The edit was committed; display delivery is pending. Retry this operation to restore the current view.",
"actionReview.apply_failed": "Execution did not complete. Check the failure and regenerate the preview before retrying.",
"actionReview.inactive_proposal": "This proposal is no longer ready to execute. Recheck it before continuing.",
"proposal.gate.default": "Host confirmation required",
Expand Down Expand Up @@ -1722,6 +1726,10 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"actionReview.readback_unverified": "操作返回但未通过读回验证。尚不能确认完成,请重新检查状态。",
"actionReview.operation_group_confirmation": "这份精确请求只能在已绑定飞书群的原始卡片确认;Dashboard 不提供本地执行入口。",
"actionReview.operation_result_delivery_pending": "操作结果已经记录,但原群结果卡尚未通过回读核验。",
"drawer.recoverEditResult": "恢复编辑结果",
"drawer.retryOriginal": "重试原操作",
"actionReview.canonical_update_retry": "编辑结果尚未确认。重试此操作以恢复原结果。",
"actionReview.canonical_update_projection_pending": "编辑已提交,展示尚未同步。重试此操作以恢复当前视图。",
"actionReview.apply_failed": "执行未完成。请检查失败原因并重新生成预览后再试。",
"actionReview.inactive_proposal": "此提案当前不可执行。请重新检查后再继续。",
"proposal.gate.default": "需要宿主确认",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ function workspaceProposal(proposal: TypedActionProposal, t: WorkspaceTranslate)
? teamPlanFields(proposal.normalized_parameters, t)
: proposalFields(proposal.normalized_parameters, t),
goalId: typeof proposal.normalized_parameters.goal_id === "string" ? proposal.normalized_parameters.goal_id : undefined,
impact: proposal.action_kind === "operation.execute"
impact: reviewPlan.retryOriginal ? t(`actionReview.${reviewPlan.reason}`) : proposal.action_kind === "operation.execute"
? t("proposal.impact.operation")
: proposal.action_kind === "team.plan"
? proposal.status === "applied" ? t("proposal.teamPlan.assignedHint") : t("proposal.impact.teamPlan")
Expand All @@ -618,7 +618,7 @@ function workspaceProposal(proposal: TypedActionProposal, t: WorkspaceTranslate)
nextAction: typeof proposal.gate.next_action === "string" ? proposal.gate.next_action : undefined,
summary: String(proposal.gate.summary ?? t("proposal.gate.default")),
} : undefined,
primaryLabel: proposal.action_kind === "operation.execute"
primaryLabel: reviewPlan.retryOriginal ? t("drawer.retryOriginal") : proposal.action_kind === "operation.execute"
? operationFrame?.kind === "result"
? operationFrame.resultDeliveryVerified
? t("proposal.primary.operationResultVerified")
Expand All @@ -635,7 +635,7 @@ function workspaceProposal(proposal: TypedActionProposal, t: WorkspaceTranslate)
: proposal.action_kind === "todo.create" && proposal.normalized_parameters.start_execution === true
? t("proposal.primary.todoStart")
: t("proposal.primary.apply"),
status: proposal.status === "applied"
status: reviewPlan.retryOriginal ? "error" : proposal.status === "applied"
&& proposal.action_kind !== "operation.execute"
&& reviewPlan.interaction !== "completed"
? "error"
Expand Down Expand Up @@ -948,6 +948,7 @@ export function PersonalWorkspacePage({
const projected = [...new Map(merged.map((item) => [item.id, item])).values()]
.filter((item) => item.kind !== "proposal"
|| !["stale", "error"].includes(item.proposal.status)
|| item.proposal.reviewPlan?.retryOriginal === true
|| sessionProposalIds.includes(item.proposal.previewId));
return projected.filter((item) => {
if (!selectedGoalId) return true;
Expand Down Expand Up @@ -1104,6 +1105,7 @@ export function PersonalWorkspacePage({
if (cancelled) return;
const restoreable = stored
.filter((proposal) => ["preview_ready", "gated", "deferred", "applying"].includes(proposal.status)
|| compileActionReviewPlan(proposal).retryOriginal === true
|| (proposal.action_kind === "operation.execute" && proposal.status === "applied"))
.map((proposal) => workspaceProposal(proposal, t));
const restored = Object.fromEntries(restoreable.map((proposal) => [proposal.previewId, proposal]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2879,8 +2879,16 @@ relax D1–D3; permanent Markdown display and the remaining roadmap stay intact.
The ownership-edit slice now uses the same typed authoring and lifecycle boundary
after promotion as the existing update transaction. It preserves claim/exclusion
fences and rejects leased ownership rewrites; legacy Markdown writing remains a
compatibility path before promotion. This removes a duplicate decision route but
does not qualify a provider, change promotion defaults, or relax D1–D3.
compatibility path before promotion. Reviewed Chat Todo/Monitor nonterminal edits
now carry validated lifecycle grants/reasons through that owner instead of
falling into the fenced legacy writer. Preview binds the provider revision and
registry witness; apply uses CAS or recovers the original immutable operation.
Current-head display recovery and reloadable Dashboard retry close this L2/L5
journey. Exclusion/binding and lease restrictions remain independent of a grant.
This removes a duplicate decision route but does not qualify a provider, change
promotion defaults, or relax D1–D3. PostgreSQL exercises the same transaction
through its isolated service/store factory; this is not Python production-route
qualification or a default-provider switch.

Capability-gap consumers now share the TS requirement/resolution owner across
legacy and canonical inputs, including quota's Monitor capability partition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2284,8 +2284,14 @@ promotion 后不再读取本地旧 lease 文件;canonical 空租约集合保
资格化,不改变 CAS/replay 或 D1–D3;永久 Markdown 展示与后续规划继续保留。
ownership 编辑在 promotion 后现在与现有 update transaction 共用 typed authoring
和 lifecycle 边界。claim/exclusion 门禁保留,带 lease 的 ownership 重写继续拒绝;
promotion 前仍保留 Markdown writer 兼容路径。这删除了一条重复决策路径,但不代表
provider 已资格化、不改变 promotion 默认值,也不放宽 D1–D3。
promotion 前仍保留 Markdown writer 兼容路径。经过审阅的 Chat Todo/Monitor 非终态
编辑现通过该 owner 传递已验证的 lifecycle grant/reason,不再落入被 fence 的旧
writer。Preview 绑定 provider revision 与 registry 见证;apply 通过 CAS 提交新
操作,或恢复原不可变回执。当前 head 的展示恢复、Dashboard 重载后可发现的重试
闭合这一 L2/L5 链路;exclusion/binding 与 lease 限制不因 grant 而消失。这删除了
重复决策路径,但不代表 provider 已资格化、不改变默认值,也不放宽 D1–D3。
PostgreSQL 经隔离 service/store factory 验证同一事务,不代表 Python 生产路由或
默认 provider 已完成切换。

命令清单、update/monitor 事务和 consumer 删除统一按
[TS 执行卡](typescript-control-plane-migration-v0.zh-CN.md#当前-stack-合入后的执行卡)
Expand Down
Loading
Loading