diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts index 43c1c20fa8..68df1d2ae1 100644 --- a/apps/presentation/dashboard/src/data/chat.ts +++ b/apps/presentation/dashboard/src/data/chat.ts @@ -294,8 +294,29 @@ export const typedActionKindSchema = z.enum([ "monitor.update", "gate.resolve", "run.correct", + "operation.execute", ]); +const typedOperationEnvelopeSchema = z.object({ + schema_version: z.literal("loopx_operation_envelope_v0"), + lifecycle_state: z.enum([ + "prepared", + "awaiting_confirmation", + "claimed", + "outcome_observed", + ]), + operation_id: z.string().min(1), + confirmation_digest: z.string().min(1), + payload_digest: z.string().min(1), + projection_digest: z.string().min(1), + expires_at: z.string().min(1), + delivery: z.record(z.string(), z.unknown()).nullable(), + confirmation: z.record(z.string(), z.unknown()).nullable(), + claim: z.record(z.string(), z.unknown()).nullable(), + outcome: z.record(z.string(), z.unknown()).nullable(), + result_delivery: z.record(z.string(), z.unknown()).nullable().optional(), +}).passthrough(); + export const typedActionProposalSchema = z.object({ schema_version: z.literal("loopx_chat_action_proposal_v1"), proposal_id: z.string().min(1), @@ -315,6 +336,7 @@ export const typedActionProposalSchema = z.object({ error: z.record(z.string(), z.unknown()).nullable().optional(), checkpoint: z.record(z.string(), z.unknown()).nullable().optional(), regenerated_from: z.string().nullable().optional(), + operation: typedOperationEnvelopeSchema.nullable().optional(), created_at: z.string(), updated_at: z.string(), }); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/action-review-plan.ts b/apps/presentation/dashboard/src/features/personal-workspace/action-review-plan.ts index 407cc86e9b..b7f492bdb6 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/action-review-plan.ts +++ b/apps/presentation/dashboard/src/features/personal-workspace/action-review-plan.ts @@ -20,6 +20,7 @@ export function compileActionReviewPlan(proposal: TypedActionProposal): ActionRe if ((lifecycle && proposal.gate != null) || proposal.status === "gated") return held("gated", "authority_gate"); if ((lifecycle && proposal.stale != null) || proposal.status === "stale") return held("refresh", "stale_proposal"); if (proposal.status === "applied") return proposal.receipt?.projection_verified === true + && (proposal.action_kind !== "operation.execute" || proposal.operation?.result_delivery != null) ? held("completed", "readback_verified") : held("repair", "readback_unverified"); if (proposal.status === "applying") return held("pending", "apply_pending"); if (proposal.status === "failed" || proposal.error != null) return held("repair", "apply_failed"); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/channel-timeline.tsx b/apps/presentation/dashboard/src/features/personal-workspace/channel-timeline.tsx index ee083c2e19..9556112dfc 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/channel-timeline.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/channel-timeline.tsx @@ -64,7 +64,7 @@ export function ChannelTimeline({ ); } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx index 8ade7a2815..8d3f250579 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx @@ -963,17 +963,21 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention {selection.item.actionKind} · {selection.item.status}
{selection.item.impact}
- {selection.item.reviewPlan ?{t(`actionReview.${selection.item.reviewPlan.reason}`)}
: null} + {selection.item.reviewPlan ?{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") + : t(`actionReview.${selection.item.reviewPlan.reason}`)}
: null} {selection.item.status === "ready" ?{t("drawer.proposalExplainer")}
: null}{t("drawer.proposalStale")}
: null} {selection.item.status === "error" ?{t("drawer.proposalRejected")}
: null} {selection.item.status === "deferred" ?{t("drawer.proposalDeferred")}
: null} - {selection.item.status === "gated" ?