create-principles-disciple 1.145.35 → 1.145.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/console/dist/server/models/ApprovalsConsoleModel.d.ts +22 -2
  3. package/console/dist/server/models/ApprovalsConsoleModel.js +56 -16
  4. package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +8 -0
  5. package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +3 -2
  6. package/console/dist/server/models/approval-review-material.d.ts +15 -0
  7. package/console/dist/server/models/approval-review-material.js +32 -0
  8. package/console/dist/server/routes/approvals.js +32 -7
  9. package/console/dist/ui/api.d.ts +8 -2
  10. package/console/dist/ui/api.js +8 -4
  11. package/console/dist/ui/i18n/en.json +98 -7
  12. package/console/dist/ui/i18n/zh-CN.json +98 -7
  13. package/console/dist/ui/pages/focus/ApprovalSubjectReading.d.ts +24 -0
  14. package/console/dist/ui/pages/focus/ApprovalSubjectReading.js +133 -0
  15. package/console/dist/ui/pages/focus/FocusPage.d.ts +5 -26
  16. package/console/dist/ui/pages/focus/FocusPage.js +166 -459
  17. package/console/dist/ui/pages/focus/OwnerWorkbench.d.ts +19 -0
  18. package/console/dist/ui/pages/focus/OwnerWorkbench.js +75 -0
  19. package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.d.ts +4 -1
  20. package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.js +7 -3
  21. package/console/dist/ui/pages/focus/focus-validation.js +31 -0
  22. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +110 -86
  23. package/console/dist/ui/pages/principles/PrinciplesPage.js +39 -30
  24. package/console/dist/ui/utils/approval-review.d.ts +30 -0
  25. package/console/dist/ui/utils/approval-review.js +50 -0
  26. package/console/dist/ui/utils/reading-layout.d.ts +1 -0
  27. package/console/dist/ui/utils/reading-layout.js +1 -1
  28. package/console/dist/ui/utils/validators.d.ts +4 -0
  29. package/console/dist/ui/utils/validators.js +10 -2
  30. package/console/dist/web/assets/app.css +119 -11
  31. package/console/dist/web/assets/app.js +40 -30
  32. package/core/dist/runtime-v2/activation/activation-types.d.ts +12 -2
  33. package/core/dist/runtime-v2/activation/activation-types.d.ts.map +1 -1
  34. package/core/dist/runtime-v2/activation/activation-types.js.map +1 -1
  35. package/core/dist/runtime-v2/activation/approval-queue.d.ts +3 -2
  36. package/core/dist/runtime-v2/activation/approval-queue.d.ts.map +1 -1
  37. package/core/dist/runtime-v2/activation/approval-queue.js +6 -4
  38. package/core/dist/runtime-v2/activation/approval-queue.js.map +1 -1
  39. package/core/dist/runtime-v2/activation/approval-review-guard.d.ts +26 -0
  40. package/core/dist/runtime-v2/activation/approval-review-guard.d.ts.map +1 -0
  41. package/core/dist/runtime-v2/activation/approval-review-guard.js +25 -0
  42. package/core/dist/runtime-v2/activation/approval-review-guard.js.map +1 -0
  43. package/core/dist/runtime-v2/activation/memory-approval-store.d.ts +6 -2
  44. package/core/dist/runtime-v2/activation/memory-approval-store.d.ts.map +1 -1
  45. package/core/dist/runtime-v2/activation/memory-approval-store.js +26 -2
  46. package/core/dist/runtime-v2/activation/memory-approval-store.js.map +1 -1
  47. package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts +6 -2
  48. package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts.map +1 -1
  49. package/core/dist/runtime-v2/activation/sqlite-approval-store.js +59 -2
  50. package/core/dist/runtime-v2/activation/sqlite-approval-store.js.map +1 -1
  51. package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts +24 -0
  52. package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts.map +1 -0
  53. package/core/dist/runtime-v2/diagnostician-association-shadow.js +84 -0
  54. package/core/dist/runtime-v2/diagnostician-association-shadow.js.map +1 -0
  55. package/core/dist/runtime-v2/diagnostician-association-validation.d.ts +18 -0
  56. package/core/dist/runtime-v2/diagnostician-association-validation.d.ts.map +1 -0
  57. package/core/dist/runtime-v2/diagnostician-association-validation.js +47 -0
  58. package/core/dist/runtime-v2/diagnostician-association-validation.js.map +1 -0
  59. package/core/dist/runtime-v2/diagnostician-output.d.ts +123 -0
  60. package/core/dist/runtime-v2/diagnostician-output.d.ts.map +1 -1
  61. package/core/dist/runtime-v2/diagnostician-output.js +41 -0
  62. package/core/dist/runtime-v2/diagnostician-output.js.map +1 -1
  63. package/core/dist/runtime-v2/index.d.ts +2 -0
  64. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  65. package/core/dist/runtime-v2/index.js +1 -0
  66. package/core/dist/runtime-v2/index.js.map +1 -1
  67. package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts +12 -1
  68. package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts.map +1 -1
  69. package/core/dist/runtime-v2/internalization/diag-router-runner.js +50 -2
  70. package/core/dist/runtime-v2/internalization/diag-router-runner.js.map +1 -1
  71. package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts +45 -0
  72. package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts.map +1 -0
  73. package/core/dist/runtime-v2/internalization/router-context-evidence.js +64 -0
  74. package/core/dist/runtime-v2/internalization/router-context-evidence.js.map +1 -0
  75. package/core/package.json +1 -1
  76. package/package.json +1 -1
  77. package/pd-cli/package.json +1 -1
  78. package/plugin/dist/bundle.js +395 -395
  79. package/release-manager/package.json +1 -1
@@ -1,23 +1,19 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useRef, useCallback } from "react";
3
- import { Link, useNavigate } from "react-router-dom";
3
+ import { Link } from "react-router-dom";
4
4
  import { useTranslation } from "react-i18next";
5
- import { toast } from "sonner";
6
5
  import { PageShell } from "../../components/layout/page-shell.js";
7
6
  import { PageLoading } from "../../components/layout/page-loading.js";
8
7
  import { SectionTitle } from "../../components/layout/section-title.js";
9
8
  import { DailyThoughtCard } from "../../components/focus/daily-thought-card.js";
10
9
  import { ShinyText } from "../../components/ui/shiny-text.js";
11
- import { fetchGovernanceQueue, fetchApprovalsGrouped, approveApproval, rejectApproval, editApproval, fetchAllActivations, fetchOwnerDecisions, } from "../../api.js";
10
+ import { fetchGovernanceQueue, fetchApprovalsGrouped, fetchAllActivations, fetchOwnerDecisions, fetchRuleBindingQueue, } from "../../api.js";
12
11
  import { selectApprovalGroupDisplayTitle, showsUnlinkedCandidateNote, validateApprovalsGroupedData } from "./focus-validation.js";
13
12
  import { splitReadingTitleBody } from "../../utils/reading-layout.js";
13
+ import { OwnerWorkbench } from "./OwnerWorkbench.js";
14
+ import { ApprovalSubjectReading } from "./ApprovalSubjectReading.js";
14
15
  import { OwnerDecisionCard } from "./OwnerDecisionCard.js";
15
- import { RuleCandidateBindingPanel } from './RuleCandidateBindingPanel.js';
16
- import { localizeApprovalWarning, splitApprovalWarnings } from "../../utils/approval-warning-localization.js";
17
- /** PRI-908: how long the decided card stays visible before the pending list refresh removes it. */
18
- const DECIDED_REFRESH_DELAY_MS = 1600;
19
- /** PRI-908: per decision, at most this many warning toasts render individually; the rest aggregate into one (rc-9: nothing dropped). */
20
- const MAX_WARNING_TOASTS = 3;
16
+ import { RuleCandidateBindingPanel, deriveRuleBindingCardState } from './RuleCandidateBindingPanel.js';
21
17
  import { fetchGovernanceExperience, fetchOwnerDecisionInbox } from "../../api.js";
22
18
  /**
23
19
  * PRI-889: 「需要你决定」区的决策卡来源选择。
@@ -77,59 +73,23 @@ export function partitionInboxForDisplay(inbox, actionCardPrincipleIds) {
77
73
  }
78
74
  /**
79
75
  * SPEC-PD-UIUX-001 R2 (Owner review round 2): the conclusion strip's headline
80
- * and total must agree with the objects actually on screen and with the inbox
81
- * response contract.
76
+ * must never claim the system is clear when the inbox response cannot back
77
+ * that claim.
82
78
  *
83
79
  * Modes (display-only; no permission derivation):
84
80
  * - 'unavailable' — inbox projection off (feature_disabled surfaces as
85
- * ownerInbox===null with NO error) or the request failed: the pending count
86
- * CANNOT be confirmed; never allClear.
81
+ * ownerInbox===null with NO error) or the request failed.
87
82
  * - 'degraded' — the inbox answered HTTP-success but carries
88
83
  * `degraded: { reason, nextAction }` (e.g. principle ledger unreadable, the
89
- * server then returns EMPTY groups): counts cannot be trusted; the strip
90
- * shows the server's reason + nextAction and never allClear.
91
- * - 'ok' — counts are trusted.
92
- *
93
- * Counting (S2): displayTotal = actionableCount + inbox pending items NOT
94
- * already represented by an on-screen action card. The association is the
95
- * verified principleId field (partition.unmatchedDecisions are inbox items
96
- * WITHOUT a card). One inbox item matching N cards of the same principle
97
- * counts the N cards (each is a distinct decision); disjoint sets sum.
84
+ * server then returns EMPTY groups): the strip shows the server's reason.
85
+ * - 'ok' — the inbox answered without degradation.
98
86
  */
99
87
  export function deriveConclusionDisplay(input) {
100
- if (input.ownerInbox === null || input.ownerInboxError !== null || input.partition === null) {
101
- return { mode: 'unavailable', displayTotal: input.actionableCount, actionableCount: input.actionableCount };
102
- }
103
- const matchedDecisions = input.ownerInbox.groups.decision.length - input.partition.unmatchedDecisions.length;
104
- const uncoveredInboxPending = Math.max(0, input.partition.pendingTotal - matchedDecisions);
105
- const base = {
106
- actionableCount: input.actionableCount,
107
- };
108
- if (input.ownerInbox.degraded !== undefined) {
109
- return {
110
- mode: 'degraded',
111
- displayTotal: input.actionableCount + uncoveredInboxPending,
112
- ...base,
113
- degraded: input.ownerInbox.degraded,
114
- };
115
- }
116
- return {
117
- mode: 'ok',
118
- displayTotal: input.actionableCount + uncoveredInboxPending,
119
- ...base,
120
- };
121
- }
122
- /**
123
- * SPEC-PD-UIUX-001 R2 (Owner review S3): EVERY conclusion branch must express
124
- * governance readiness the same way — "actionable now" is only claimable when
125
- * the actions are not locked. One matrix, one source.
126
- */
127
- export function resolveConclusionActionableKey(mode, locked) {
128
- if (locked)
129
- return "pages.focus.conclusion.actionableLocked";
130
- if (mode === 'unavailable')
131
- return "pages.focus.conclusion.actionableWithoutTotal";
132
- return "pages.focus.conclusion.actionableUnlocked";
88
+ if (input.ownerInbox === null || input.ownerInboxError !== null)
89
+ return { mode: 'unavailable' };
90
+ if (input.ownerInbox.degraded !== undefined)
91
+ return { mode: 'degraded', degraded: input.ownerInbox.degraded };
92
+ return { mode: 'ok' };
133
93
  }
134
94
  // ── i18n code mapping helpers ─────────────────────────────────────────────
135
95
  /** Map a stateReasonCode to an i18n key, with optional interpolation params. */
@@ -174,19 +134,6 @@ export function deriveOwnerConfigureGuide(ownerIdentityConfiguration) {
174
134
  return null;
175
135
  return { docUrl: OWNER_CONFIGURE_DOC_URL, commands: OWNER_CONFIGURE_COMMANDS };
176
136
  }
177
- // ── Channel label helper ─────────────────────────────────────────────────────
178
- function getChannelLabel(channel, t) {
179
- switch (channel) {
180
- case "prompt":
181
- return t("pages.focus.channelPrompt");
182
- case "defer_archive":
183
- return t("pages.focus.channelDeferArchive");
184
- case "code_tool_hook":
185
- return t("pages.focus.channelCodeToolHook");
186
- default:
187
- return channel;
188
- }
189
- }
190
137
  // ── Sub-components ───────────────────────────────────────────────────────────
191
138
  function ProseSummary({ pendingCount, deviationCount, stagnationCount, evidenceCount, }) {
192
139
  const { t } = useTranslation();
@@ -204,293 +151,6 @@ function FeedbackStratification({ gateBlocksToday, inProgressCount, pendingCount
204
151
  }
205
152
  return (_jsxs("div", { className: "mb-7 px-[18px] py-[14px] bg-panel border border-line rounded-[6px]", children: [_jsx("div", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4 mb-3", children: t("pages.focus.stratLabel") }), _jsxs("div", { className: "flex items-stretch gap-3 flex-wrap", children: [_jsxs("div", { className: "flex-1 min-w-[140px] px-3 py-2 bg-surface/60 border border-line rounded-[4px]", children: [_jsxs("div", { className: "flex items-baseline gap-1.5", children: [_jsx("span", { className: "font-mono font-semibold text-[18px] text-ink-4 tabular-nums", children: gateBlocksToday }), _jsx("span", { className: "text-[12px] text-ink-3", children: t("pages.focus.stratGateBlocks") })] }), _jsx("div", { className: "text-[11px] text-ink-4 mt-0.5 leading-snug", children: t("pages.focus.stratGateBlocksHint") })] }), _jsxs("div", { className: "flex-1 min-w-[140px] px-3 py-2 bg-surface/60 border border-line rounded-[4px]", children: [_jsxs("div", { className: "flex items-baseline gap-1.5", children: [_jsx("span", { className: "font-mono font-semibold text-[18px] text-teal-600 tabular-nums", children: inProgressCount }), _jsx("span", { className: "text-[12px] text-ink-3", children: t("pages.focus.stratInProgress") })] }), _jsx("div", { className: "text-[11px] text-ink-4 mt-0.5 leading-snug", children: t("pages.focus.stratInProgressHint") })] }), _jsxs("div", { className: "flex-1 min-w-[140px] px-3 py-2 bg-gov/5 border border-gov/30 rounded-[4px] relative", children: [_jsxs("div", { className: "flex items-baseline gap-1.5", children: [_jsx("span", { className: "font-mono font-semibold text-[18px] text-gov tabular-nums", children: pendingCount }), _jsx("span", { className: "text-[12px] text-gov", children: t("pages.focus.stratPendingReview") })] }), _jsx("div", { className: "text-[11px] text-ink-4 mt-0.5 leading-snug", children: t("pages.focus.stratPendingReviewHint") }), _jsx("span", { className: "absolute -top-2 right-2 inline-flex items-center border border-gov/40 text-gov bg-panel rounded-[2px] px-[6px] py-0.5 font-mono text-[9px] uppercase tracking-wider", children: t("pages.focus.stratYouAreHere") })] })] })] }));
206
153
  }
207
- function PendingReviewCard({ group, actionsLockedReason, onDecisionApplied, promptInjection, promptTargetHost, }) {
208
- const { t } = useTranslation();
209
- const navigate = useNavigate();
210
- const primaryChannel = group.records[0]?.channel ?? "prompt";
211
- const channelLabel = getChannelLabel(primaryChannel, t);
212
- const isReversible = primaryChannel === "prompt" || primaryChannel === "defer_archive";
213
- const actionsLocked = actionsLockedReason !== undefined;
214
- // Inline review state (Wave 7: no more 404 jump to /principles/<fake-id>)
215
- const [actionLoading, setActionLoading] = useState(false);
216
- const [intentReviewed, setIntentReviewed] = useState(false);
217
- useEffect(() => setIntentReviewed(false), [group]);
218
- const [showRejectInput, setShowRejectInput] = useState(false);
219
- const [rejectReason, setRejectReason] = useState("");
220
- const [showEditInput, setShowEditInput] = useState(false);
221
- const [editReason, setEditReason] = useState("");
222
- const [newArtifactId, setNewArtifactId] = useState("");
223
- // PRI-908: 决策成功后卡片先呈现终态(已批准/已拒绝)短暂停留,再刷新待办
224
- // 列表移除卡片——避免"点了就消失"让 Owner 无法确认操作是否落地。
225
- const [decidedOutcome, setDecidedOutcome] = useState(null);
226
- const refreshTimerRef = useRef(null);
227
- useEffect(() => () => {
228
- if (refreshTimerRef.current !== null)
229
- window.clearTimeout(refreshTimerRef.current);
230
- }, []);
231
- const isActionable = group.status === "pending" && group.records.some((r) => r.status === "pending");
232
- const decisionDisabled = !isActionable || actionLoading || actionsLocked || decidedOutcome !== null;
233
- function scheduleDecisionRefresh() {
234
- if (refreshTimerRef.current !== null)
235
- window.clearTimeout(refreshTimerRef.current);
236
- refreshTimerRef.current = window.setTimeout(() => onDecisionApplied(), DECIDED_REFRESH_DELAY_MS);
237
- }
238
- /** PRI-890/908: 非致命服务端警告必须抵达 Owner(rc-9),且必须说人话。 */
239
- function notifySuccessWarnings(successWarnings) {
240
- const segments = successWarnings.flatMap((warning) => splitApprovalWarnings(warning));
241
- const localized = segments.map((warning) => localizeApprovalWarning(warning, t));
242
- // PR-1894: the raw server text is still preserved verbatim (rc-9 — never
243
- // dropped), but it is operator-facing English, not Owner-facing prose. It
244
- // used to be appended flat under the localized body, so the toast rendered
245
- // as one mixed Chinese/English block and the machine text visually
246
- // dominated the message. Collapse it behind a native <details> so the
247
- // Owner reads plain language first and can still open the raw evidence.
248
- const renderDetail = (detail) => (_jsxs("details", { className: "mt-2 text-[11.5px] opacity-80", children: [_jsx("summary", { className: "cursor-pointer select-none", children: t("pages.focus.approveWarningDetail", { defaultValue: "原始信息" }) }), _jsx("pre", { className: "mt-1 max-h-40 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px]", children: detail })] }));
249
- for (const item of localized.slice(0, MAX_WARNING_TOASTS)) {
250
- toast.warning(item.title, {
251
- description: item.body
252
- ? (_jsxs(_Fragment, { children: [item.body, renderDetail(item.detail)] }))
253
- // An unrecognized code degrades to the raw string as the whole
254
- // message (rc-9) — there is no localized body to sit above it.
255
- : item.detail,
256
- duration: 15000,
257
- action: item.activationAction
258
- ? { label: t("pages.focus.goActivationCta", { defaultValue: "前往生效情况" }), onClick: () => navigate("/activation") }
259
- : undefined,
260
- });
261
- }
262
- const overflow = localized.slice(MAX_WARNING_TOASTS);
263
- if (overflow.length > 0) {
264
- toast.warning(t("pages.focus.approveWarning.overflowTitle", { defaultValue: "还有 {{count}} 条警告", count: overflow.length }), {
265
- description: (_jsx(_Fragment, { children: overflow.map((item) => (_jsxs("div", { className: "mb-1 last:mb-0", children: [_jsx("div", { children: item.title }), item.body ? _jsx("div", { children: item.body }) : null, renderDetail(item.detail)] }, item.detail))) })),
266
- duration: 15000,
267
- });
268
- }
269
- }
270
- // Display title: prefer candidateDescription (human-readable) over principleTitle
271
- // (which falls back to a fabricated principleId when the principle isn't in ledger).
272
- // The selection rule and each degradation reason live in focus-validation
273
- // (PRI-940 / PRI-941, contract-tested); the mutually exclusive notes below say
274
- // why a particular card has no title.
275
- const displayTitle = selectApprovalGroupDisplayTitle(group)
276
- ?? t("pages.focus.untitledCandidate", { defaultValue: "待命名候选原则" });
277
- const unlinkedCandidateNote = showsUnlinkedCandidateNote(group);
278
- // SPEC-PD-CONSOLE-READABILITY-002 切片1(R2/R3/R7):候选描述可能是整段规则
279
- // 正文——不再整段充当加粗标题。短标题只作预览,逐字全文由正文块承载。
280
- const readingLayout = splitReadingTitleBody(displayTitle);
281
- async function applyDecisionToAllRecords(action, reason) {
282
- // Only process pending records — skip already-approved/rejected to avoid
283
- // stable partial failures on mixed-status groups.
284
- const pendingRecords = group.records.filter((r) => r.status === "pending");
285
- const results = [];
286
- for (const record of pendingRecords) {
287
- const result = action === "approve"
288
- ? await approveApproval(record.id, undefined, { targetHost: promptTargetHost, intentReviewed: group.revisionReview === undefined ? undefined : intentReviewed, reviewedArtifactId: record.artifactId })
289
- : await rejectApproval(record.id, reason ?? "");
290
- // PRI-890: preserve non-fatal server warnings (e.g. prompt injection
291
- // budget exclusion) on successful decisions.
292
- results.push(result.success
293
- ? { success: true, warning: result.data?.warning }
294
- : { success: false, error: result.error ?? "unknown error", nextAction: result.nextAction });
295
- }
296
- return { ...summarizeDecisionResults(results), totalCount: pendingRecords.length };
297
- }
298
- const handleApprove = async () => {
299
- if (!isActionable || actionLoading || decidedOutcome !== null || (group.revisionReviewUnavailable === true || (group.revisionReview !== undefined && !intentReviewed)))
300
- return;
301
- setActionLoading(true);
302
- try {
303
- const { allSucceeded, failedCount, totalCount, failureReason, successWarnings } = await applyDecisionToAllRecords("approve");
304
- if (allSucceeded) {
305
- toast.success(t("pages.focus.approveSucceededCount", { defaultValue: "已批准 {{count}} 条原则", count: totalCount }));
306
- // PRI-890: a committed activation can still be excluded from agent
307
- // behavior (prompt injection budget saturated) — the server warning
308
- // must reach the Owner, not vanish behind the success toast (rc-9).
309
- // PRI-908: warnings render as localized plain-language toasts with an
310
- // activation-page action, not raw English text.
311
- notifySuccessWarnings(successWarnings);
312
- setDecidedOutcome("approved");
313
- scheduleDecisionRefresh();
314
- }
315
- else {
316
- toast.error(t("pages.focus.partialFailure", {
317
- defaultValue: `批准完成,但 ${failedCount}/${totalCount} 条记录失败。`,
318
- failedCount,
319
- totalCount,
320
- reason: failureReason ?? t("pages.focus.unknownFailure", { defaultValue: "请检查服务日志。" }),
321
- }));
322
- onDecisionApplied();
323
- }
324
- }
325
- catch {
326
- toast.error(t("pages.focus.approveFailed", { defaultValue: "批准失败,请稍后重试。" }));
327
- }
328
- finally {
329
- setActionLoading(false);
330
- }
331
- };
332
- const handleReject = async () => {
333
- if (!isActionable || actionLoading || decidedOutcome !== null || !rejectReason.trim())
334
- return;
335
- setActionLoading(true);
336
- try {
337
- const { allSucceeded, failedCount, totalCount, failureReason } = await applyDecisionToAllRecords("reject", rejectReason.trim());
338
- if (allSucceeded) {
339
- toast.success(t("pages.focus.rejectSucceeded", { defaultValue: "已拒绝" }));
340
- setShowRejectInput(false);
341
- setRejectReason("");
342
- setDecidedOutcome("rejected");
343
- scheduleDecisionRefresh();
344
- }
345
- else {
346
- toast.error(t("pages.focus.partialFailure", {
347
- defaultValue: `拒绝完成,但 ${failedCount}/${totalCount} 条记录失败。`,
348
- failedCount,
349
- totalCount,
350
- reason: failureReason ?? t("pages.focus.unknownFailure", { defaultValue: "请检查服务日志。" }),
351
- }));
352
- onDecisionApplied();
353
- }
354
- }
355
- catch {
356
- toast.error(t("pages.focus.rejectFailed", { defaultValue: "拒绝失败,请稍后重试。" }));
357
- }
358
- finally {
359
- setActionLoading(false);
360
- }
361
- };
362
- const currentArtifactId = group.records.find((r) => r.status === "pending")?.artifactId ?? "";
363
- const handleEdit = async () => {
364
- if (!isActionable || actionLoading || decidedOutcome !== null || !editReason.trim() || !newArtifactId.trim())
365
- return;
366
- setActionLoading(true);
367
- try {
368
- const pendingRecords = group.records.filter((r) => r.status === "pending");
369
- const results = [];
370
- for (const record of pendingRecords) {
371
- const result = await editApproval(record.id, newArtifactId.trim(), editReason.trim());
372
- results.push(result.success
373
- ? { success: true }
374
- : { success: false, error: result.error, nextAction: result.nextAction });
375
- }
376
- const { allSucceeded, failedCount, failureReason } = summarizeDecisionResults(results);
377
- const totalCount = results.length;
378
- if (allSucceeded) {
379
- toast.success(t("pages.focus.editSucceeded", { defaultValue: "已替换待审版本" }));
380
- setShowEditInput(false);
381
- setEditReason("");
382
- setNewArtifactId("");
383
- onDecisionApplied();
384
- }
385
- else {
386
- toast.error(t("pages.focus.partialFailure", {
387
- defaultValue: `替换完成,但 ${failedCount}/${totalCount} 条记录失败。`,
388
- failedCount,
389
- totalCount,
390
- reason: failureReason ?? t("pages.focus.unknownFailure", { defaultValue: "请检查服务日志。" }),
391
- }));
392
- onDecisionApplied();
393
- }
394
- }
395
- catch {
396
- toast.error(t("pages.focus.editFailed", { defaultValue: "替换失败,请稍后重试。" }));
397
- }
398
- finally {
399
- setActionLoading(false);
400
- }
401
- };
402
- return (_jsxs("article", { className: "relative pl-[22px] py-[18px] pr-[18px] bg-panel border border-line rounded-[6px] transition-colors", children: [_jsx("div", { className: "absolute left-0 top-0 bottom-0 w-[3px] rounded-l-[6px] bg-gov" }), decidedOutcome !== null && (_jsx("div", { className: `mb-3 rounded-[3px] border px-3 py-2 text-[12.5px] ${decidedOutcome === "approved"
403
- ? "border-green/40 bg-green/10 text-green"
404
- : "border-line bg-surface/80 text-ink-3"}`, role: "status", "data-testid": `decided-banner-${group.principleId}`, children: decidedOutcome === "approved"
405
- ? t("pages.focus.decidedApproved", { defaultValue: "已批准 ✓ 正在写入生效记录…" })
406
- : t("pages.focus.decidedRejected", { defaultValue: "已拒绝" }) })), _jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("span", { className: "inline-flex items-center border border-line rounded-[2px] px-[7px] py-1 font-mono text-[11px] text-ink-3 bg-surface/80 uppercase", children: channelLabel }), isReversible && (_jsx("span", { className: "inline-flex items-center border border-amber/35 text-amber rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: primaryChannel === "prompt" ? t("pages.focus.tagReversible") : t("pages.focus.tagLowRisk") }))] }), _jsx("div", { className: "mt-[14px] mb-2 font-semibold text-ink leading-snug", "data-testid": `candidate-title-${group.principleId}`, children: readingLayout.title }), readingLayout.body !== null && (_jsx("div", { className: "mb-2 text-[15px] leading-relaxed text-ink whitespace-pre-wrap break-words", "data-testid": `candidate-body-${group.principleId}`, children: readingLayout.body })), group.artifactUnavailable === true && (_jsx("div", { className: "mb-2 text-[12.5px] leading-relaxed text-amber", "data-testid": `artifact-unavailable-${group.principleId}`, children: t("pages.focus.artifactUnavailableNote") })), unlinkedCandidateNote && (_jsx("div", { className: "mb-2 text-[12.5px] leading-relaxed text-amber", "data-testid": `unlinked-candidate-${group.principleId}`, children: t("pages.focus.unlinkedCandidateNote") })), _jsx("div", { className: "mt-2 px-3 py-2 bg-surface/60 border-l-2 border-gov text-ink-3 text-[13px] leading-snug", children: t("pages.focus.evidenceSummary", { count: group.records.length }) }), actionsLocked && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 p-3", "data-testid": `pending-actions-locked-${group.principleId}`, children: [_jsx("p", { className: "text-ink-2 text-[12.5px] leading-relaxed", children: actionsLockedReason }), _jsx(Link, { to: "/settings", "data-testid": `pending-actions-locked-settings-${group.principleId}`, className: "mt-2 inline-flex items-center border border-line text-ink bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.ownerDecision.goSettingsCta") })] })), decidedOutcome === null && primaryChannel === "prompt" && promptInjection?.truncated === true && promptInjection.capacityStatus !== "unconfirmed" && (_jsxs("div", { className: "mt-3 rounded-[3px] border border-amber/40 bg-amber/5 px-3 py-2 text-[12.5px] text-amber", "data-testid": `approve-queue-badge-${group.principleId}`, children: [promptInjection.productionRotates === true && group.fitsPromptBudget === true && group.promptBudgetTierFit === "model_dependent"
407
- ? t("pages.focus.approveQueueBadgeRotationModelDependent", {
408
- defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。这条原则的篇幅在较小上下文模型的注入预算内装不下,轮到它时也未必进入提示词——是否生效取决于当时模型的上限。",
409
- used: promptInjection.usedChars,
410
- budget: promptInjection.budget,
411
- })
412
- : promptInjection.productionRotates === true && group.fitsPromptBudget === true
413
- ? t("pages.focus.approveQueueBadgeRotation", {
414
- defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。当前按公平轮转选取,新原则会在后续轮次自行轮到,无需停用旧原则。",
415
- used: promptInjection.usedChars,
416
- budget: promptInjection.budget,
417
- })
418
- : promptInjection.productionRotates === true
419
- ? t("pages.focus.approveQueueBadgeOversized", {
420
- defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。这条原则本身已超出注入位上限,轮转也无法让它进入提示词——需要先精简它的内容。",
421
- used: promptInjection.usedChars,
422
- budget: promptInjection.budget,
423
- })
424
- : t("pages.focus.approveQueueBadge", {
425
- defaultValue: "批准后可能排队:提示词注入位已满({{used}}/{{budget}} 字符)。",
426
- used: promptInjection.usedChars,
427
- budget: promptInjection.budget,
428
- }), _jsxs("div", { className: "mt-1 text-ink-3 text-[11.5px]", "data-testid": `approve-queue-badge-scope-${group.principleId}`, children: [promptInjection.route === "shared_render"
429
- ? t("pages.focus.promptScopeShared")
430
- : t("pages.focus.promptScopeList", {
431
- full: promptInjection.fullRenderChars !== undefined
432
- ? t("pages.focus.promptScopeFullRender", { count: promptInjection.fullRenderChars })
433
- : "",
434
- }), promptInjection.hostKind !== undefined ? ` · ${promptInjection.hostKind === "openclaw" ? "OpenClaw" : promptInjection.hostKind === "workbuddy" ? "WorkBuddy" : "Codex"}` : "", ` · ${
435
- // A "confirmed" claim is only renderable when the window is
436
- // present: a payload missing it must not print "0 tokens".
437
- promptInjection.budgetCapacityStatus === "confirmed" && promptInjection.budgetContextWindowTokens !== undefined
438
- ? promptInjection.budgetObservedAt !== undefined
439
- // PRI-1017: the budget comes from the workspace's most
440
- // recent RUNTIME OBSERVATION (the last real injection) —
441
- // say so, with source + window + observation time. Never
442
- // present a past observation as a live confirmation.
443
- ? t("pages.focus.promptBudgetObservedNote", {
444
- defaultValue: "预算取自最近一次运行时观测(来源 {{source}},上下文 {{window}} tokens,观测于 {{observedAt}})",
445
- // PRI-1020: three-way source label — the owner-declared
446
- // basis reads as a declaration, never as a runtime fact.
447
- source: promptInjection.budgetCapacitySource === "pi_catalog_exact"
448
- ? t("pages.focus.budgetCapacitySourcePiCatalog", { defaultValue: "模型目录精确匹配" })
449
- : promptInjection.budgetCapacitySource === "operator_declared"
450
- ? t("pages.focus.budgetCapacitySourceOperatorDeclared", { defaultValue: "工作区配置声明" })
451
- : t("pages.focus.budgetCapacitySourceHostWindow", { defaultValue: "宿主有效上下文窗口" }),
452
- window: promptInjection.budgetContextWindowTokens,
453
- observedAt: promptInjection.budgetObservedAt,
454
- })
455
- : t("pages.focus.promptBudgetModelAwareNote", {
456
- defaultValue: "预算按已确认的模型上下文解析({{window}} tokens{{sourceSuffix}})",
457
- window: promptInjection.budgetContextWindowTokens,
458
- // PRI-1020: without an observation, a confirmed budget can
459
- // only come from the Owner's declaration — say so.
460
- sourceSuffix: promptInjection.budgetCapacitySource === "operator_declared"
461
- ? t("pages.focus.budgetCapacitySourceOperatorDeclaredSuffix", { defaultValue: ",来源: 工作区配置声明" })
462
- : "",
463
- })
464
- : promptInjection.budgetObservedAt !== undefined
465
- // PRI-1017: a fallback that still carries an observation
466
- // timestamp IS the runtime's own honest fallback (the last
467
- // injection resolved 2000 with a reason). Saying "console
468
- // not bound to the runtime model" here would be false — the
469
- // console DID use the runtime's last observation.
470
- ? t("pages.focus.promptBudgetFallbackObservedNote", {
471
- defaultValue: "运行时最近一次观测未确认宿主容量,预算按固定回退估算({{reason}},观测于 {{observedAt}})",
472
- reason: promptInjection.budgetFallbackReason !== undefined
473
- ? t(`pages.focus.budgetFallbackReason.${promptInjection.budgetFallbackReason}`, {
474
- defaultValue: promptInjection.budgetFallbackReason,
475
- })
476
- : "unknown",
477
- observedAt: promptInjection.budgetObservedAt,
478
- })
479
- : t("pages.focus.promptBudgetFallbackNote", {
480
- defaultValue: "按固定回退预算估算(控制台未绑定运行时模型{{reason}})",
481
- reason: promptInjection.budgetFallbackReason !== undefined
482
- ? t("pages.focus.promptBudgetFallbackReasonSuffix", {
483
- defaultValue: ",原因: {{code}}",
484
- // The raw bounded code stays the default: it is the
485
- // grep-able identifier shared with events/logs. Locales
486
- // may override via pages.focus.budgetFallbackReason.<code>.
487
- code: t(`pages.focus.budgetFallbackReason.${promptInjection.budgetFallbackReason}`, {
488
- defaultValue: promptInjection.budgetFallbackReason,
489
- }),
490
- })
491
- : "",
492
- })}`] })] })), group.revisionReviewUnavailable === true && _jsx("p", { role: "alert", className: "mt-3 text-danger", children: t("pages.focus.revisionReviewUnavailable") }), group.revisionReview !== undefined && (_jsxs("div", { className: "mt-3 border border-line p-3 text-[12.5px]", "data-testid": `revision-intent-review-${group.principleId}`, children: [_jsx("p", { children: t("pages.focus.revisionIntentReview", { defaultValue: "修订复核:确认新正文保留触发条件、行动要求和必要例外,并与沿用意图一致。" }) }), _jsx("pre", { className: "mt-2 max-h-32 overflow-auto whitespace-pre-wrap", children: group.revisionReview.oldStatement }), _jsx("pre", { className: "mt-2 max-h-32 overflow-auto whitespace-pre-wrap", children: group.revisionReview.newStatement }), _jsx("dl", { className: "mt-2", children: Object.entries(group.revisionReview.intentFields).map(([key, value]) => _jsxs("div", { children: [_jsx("dt", { children: t(`pages.focus.intentField.${key}`, { defaultValue: key }) }), _jsx("dd", { children: value })] }, key)) }), _jsxs("label", { className: "mt-2 flex gap-2", children: [_jsx("input", { type: "checkbox", checked: intentReviewed, onChange: (event) => setIntentReviewed(event.target.checked) }), t("pages.focus.revisionIntentConfirmed", { defaultValue: "我已核对上述意图与新正文一致" })] })] })), _jsxs("div", { className: "flex gap-2 mt-4 flex-wrap items-center", children: [_jsx("button", { type: "button", onClick: handleApprove, disabled: decisionDisabled || (group.revisionReviewUnavailable === true || (group.revisionReview !== undefined && !intentReviewed)), "data-testid": `approve-btn-${group.principleId}`, className: "inline-flex items-center border border-gov bg-gov text-paper rounded-[3px] px-[14px] py-[6px] text-[12.5px] font-medium hover:bg-gov-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: actionLoading ? t("common.loading") + "…" : t("pages.focus.approveAction", { defaultValue: "批准" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput((v) => !v); setShowRejectInput(false); }, disabled: decisionDisabled, "data-testid": `edit-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.editAction", { defaultValue: "替换待审版本" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput((v) => !v); setShowEditInput(false); }, disabled: decisionDisabled, "data-testid": `reject-btn-${group.principleId}`, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.rejectAction", { defaultValue: "拒绝" }) }), _jsx(Link, { to: "/pain", className: "inline-flex items-center text-gov text-[12.5px] hover:underline focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.viewFullChain") })] }), showRejectInput && (_jsxs("div", { className: "mt-3 p-3 border border-danger/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.rejectReasonLabel", { defaultValue: "拒绝原因(必填)" }) }), _jsx("textarea", { value: rejectReason, onChange: (e) => setRejectReason(e.target.value), "data-testid": `reject-reason-${group.principleId}`, placeholder: t("pages.focus.rejectReasonPlaceholder", { defaultValue: "请说明拒绝原因" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleReject, disabled: !rejectReason.trim() || actionLoading, "data-testid": `confirm-reject-${group.principleId}`, className: "inline-flex items-center border border-danger text-danger bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-danger/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmReject", { defaultValue: "确认拒绝" }) }), _jsx("button", { type: "button", onClick: () => { setShowRejectInput(false); setRejectReason(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] })), showEditInput && (_jsxs("div", { className: "mt-3 p-3 border border-gov/20 rounded-[var(--radius-md)]", children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("pages.focus.editReasonLabel", { defaultValue: "替换原因(必填)" }) }), _jsxs("div", { className: "mb-2 text-ink-4 text-[12px] font-mono", children: [t("pages.focus.currentArtifactLabel", { defaultValue: "当前工件" }), ": ", currentArtifactId] }), _jsx("input", { type: "text", value: newArtifactId, onChange: (e) => setNewArtifactId(e.target.value), "data-testid": `edit-new-artifact-${group.principleId}`, placeholder: t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] mb-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("pages.focus.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }) }), _jsx("textarea", { value: editReason, onChange: (e) => setEditReason(e.target.value), "data-testid": `edit-reason-${group.principleId}`, placeholder: t("pages.focus.editReasonPlaceholder", { defaultValue: "请说明为什么需要替换这份待审版本" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov" }), _jsxs("div", { className: "flex gap-2 mt-2", children: [_jsx("button", { type: "button", onClick: handleEdit, disabled: !editReason.trim() || !newArtifactId.trim() || actionLoading, "data-testid": `confirm-edit-${group.principleId}`, className: "inline-flex items-center border border-gov text-gov bg-surface rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-gov/5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", children: t("pages.focus.confirmEdit", { defaultValue: "确认替换" }) }), _jsx("button", { type: "button", onClick: () => { setShowEditInput(false); setEditReason(""); setNewArtifactId(""); }, className: "inline-flex items-center border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors", children: t("pages.focus.cancelAction", { defaultValue: "取消" }) })] })] }))] }));
493
- }
494
154
  function StagnationSignalCard({ signal }) {
495
155
  const { t } = useTranslation();
496
156
  const label = signal.type === "never_activated"
@@ -609,6 +269,24 @@ function ExperienceSummaryCard({ snapshot }) {
609
269
  : "pages.focus.experience.blocker.authenticationMissing";
610
270
  return (_jsxs("div", { className: "mb-7 px-[18px] py-[14px] bg-panel border border-line rounded-[6px]", "data-testid": "experience-summary", children: [_jsx("div", { className: "flex items-center gap-2 mb-1", children: _jsx("span", { className: `inline-flex items-center rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase ${attention.badgeClass}`, role: "status", children: t(attention.labelKey) }) }), _jsx("div", { className: "text-ink-2 text-[13px] leading-relaxed mt-1", "data-testid": "experience-reason", children: t(EXPERIENCE_REASON[snapshot.summary.reasonCode]) }), _jsxs("div", { className: "text-ink-4 text-[13px] leading-relaxed mt-1", children: [_jsx("span", { className: "font-medium", children: t("pages.focus.nextActionLabel") }), " ", t(EXPERIENCE_NEXT_ACTION[snapshot.summary.nextActionCode])] }), (decision !== undefined || recovery !== undefined || blocked !== undefined || processing !== undefined) && (_jsxs("div", { className: "text-[13px] text-ink-3 mt-2 leading-relaxed", "data-testid": "experience-activity", children: [decision !== undefined && (_jsx("span", { className: "mr-4", children: t("pages.focus.experience.activity.needsDecision", { count: decision.count }) })), recovery !== undefined && (_jsx("span", { className: "mr-4", children: t("pages.focus.experience.activity.needsRecovery", { count: recovery.count }) })), blocked !== undefined && _jsx("span", { className: "mr-4", children: t("pages.focus.experience.activity.blocked") }), processing !== undefined && (_jsx("span", { children: t("pages.focus.experience.activity.processing", { count: processing.count }) }))] })), governanceBlocked && (_jsxs("div", { className: "mt-3 flex items-center justify-between gap-3 border border-amber/30 rounded-[6px] bg-surface px-3 py-2.5", "data-testid": "owner-configure-guide", children: [_jsx("p", { className: "text-[12px] text-ink-3 leading-relaxed", children: t(blockerKey) }), _jsx(Link, { to: "/settings", className: "shrink-0 text-[12px] text-gov underline hover:text-gov-2", children: t("pages.focus.experience.blocker.goSettings") })] })), _jsxs("div", { className: "mt-1 text-[12px] text-ink-4", "data-testid": "experience-trust", children: [t(EXPERIENCE_ENVIRONMENT[snapshot.trustContext.environmentContext.environment]), " · ", t(`pages.focus.experience.trust.lineage.${snapshot.trustContext.lineageTransparency.confidence}`)] }), snapshot.dataQuality.degraded && (_jsx("div", { className: "mt-2 text-[12px] text-amber leading-relaxed", "data-testid": "experience-data-quality", children: t("pages.focus.experience.dataQuality.degraded", { count: firstIssue?.count ?? 0, reasonCode: firstIssue?.reasonCode ?? "" }) }))] }));
611
271
  }
272
+ function decisionWorkKind(kind) {
273
+ switch (kind) {
274
+ case 'rulecode_decision': return 'promotion';
275
+ case 'evaluator_review': return 'revision';
276
+ case 'rollout_review': return 'validation';
277
+ case 'activation_approval': return 'approval';
278
+ default: return 'unknown';
279
+ }
280
+ }
281
+ function approvalWorkKind(channel) {
282
+ switch (channel) {
283
+ case 'prompt': return 'prompt';
284
+ case 'code_tool_hook': return 'rule';
285
+ case 'defer_archive': return 'archive';
286
+ default: return 'unknown';
287
+ }
288
+ }
289
+ const FOCUS_SOURCES = ['summary', 'approvals', 'activations', 'bindings', 'decisions', 'inbox'];
612
290
  export function FocusPage({ featureFlags }) {
613
291
  const { t } = useTranslation();
614
292
  // PRI-586: when governance_experience_v1 is enabled the governance summary
@@ -620,6 +298,11 @@ export function FocusPage({ featureFlags }) {
620
298
  // otherwise a transient legacy queue request goes out before the flag turns
621
299
  // experience mode on (wasted call + legacy-panel flash). Hold in `loading`.
622
300
  const flagsResolved = featureFlags !== undefined;
301
+ const [bindingQueue, setBindingQueue] = useState(null);
302
+ const [bindingError, setBindingError] = useState(null);
303
+ const [completedMessage, setCompletedMessage] = useState(null);
304
+ const requestEpoch = useRef(0);
305
+ const [pendingSources, setPendingSources] = useState(FOCUS_SOURCES);
623
306
  const [queueData, setQueueData] = useState(null);
624
307
  const [experienceData, setExperienceData] = useState(null);
625
308
  const [groupedData, setGroupedData] = useState(null);
@@ -641,82 +324,102 @@ export function FocusPage({ featureFlags }) {
641
324
  // only — no persisted host configuration is created here.
642
325
  const [promptTargetHost, setPromptTargetHost] = useState(null);
643
326
  const loadData = useCallback(async () => {
644
- setLoadingState("loading");
327
+ const epoch = ++requestEpoch.current;
328
+ setLoadingState("loaded");
329
+ setPendingSources(FOCUS_SOURCES);
330
+ setExperienceData(null);
331
+ setQueueData(null);
332
+ setGroupedData(null);
333
+ setRuleCodeItems([]);
334
+ setBindingQueue(null);
335
+ setOwnerDecisionItems([]);
336
+ setOwnerInbox(null);
645
337
  setErrorMessage(null);
646
338
  setGroupedErrorReason(null);
647
339
  setRuleCodeErrorReason(null);
648
- const [experienceResult, queueResult, groupedResult, activationsResult] = await Promise.all([
649
- experienceMode ? fetchGovernanceExperience() : Promise.resolve(null),
650
- experienceMode ? Promise.resolve(null) : fetchGovernanceQueue(),
651
- fetchApprovalsGrouped(promptTargetHost ?? undefined),
652
- fetchAllActivations(promptTargetHost ?? undefined),
653
- ]);
654
- if (experienceMode) {
655
- if (experienceResult === null || !experienceResult.success) {
656
- setLoadingState("error");
657
- setErrorMessage(experienceResult === null ? t("pages.focus.experience.error.snapshotUnavailable") : experienceResult.error);
658
- return;
340
+ setBindingError(null);
341
+ setOwnerDecisionError(null);
342
+ setOwnerInboxError(null);
343
+ // Each source publishes independently. A stalled history/inbox request must
344
+ // not hide already available decisions; old request epochs cannot publish.
345
+ async function settle(source, request, apply, fail) {
346
+ try {
347
+ const result = await request;
348
+ if (epoch === requestEpoch.current)
349
+ apply(result);
659
350
  }
660
- setExperienceData(experienceResult.data);
661
- setQueueData(null);
662
- }
663
- else {
664
- // Queue data is already validated by the API layer (validateGovernanceQueue)
665
- if (queueResult === null || !queueResult.success) {
666
- setLoadingState("error");
667
- setErrorMessage(queueResult === null ? t("pages.focus.loadError") : queueResult.error);
668
- return;
351
+ catch (error) {
352
+ if (epoch === requestEpoch.current)
353
+ fail(error instanceof Error ? error.message : t('pages.focus.loadError'));
669
354
  }
670
- setQueueData(queueResult.data);
671
- setExperienceData(null);
672
- }
673
- // Validate grouped data (ERR-002: degradation with reason)
674
- if (!groupedResult.success) {
675
- setGroupedData(null);
676
- setGroupedErrorReason(groupedResult.error ?? "Approvals data unavailable");
677
- }
678
- else {
679
- const validatedGrouped = validateApprovalsGroupedData(groupedResult.data);
680
- setGroupedData(validatedGrouped);
681
- if (validatedGrouped === null) {
682
- setGroupedErrorReason("Approvals data has unexpected shape");
355
+ finally {
356
+ if (epoch === requestEpoch.current)
357
+ setPendingSources(current => current.filter(item => item !== source));
683
358
  }
684
359
  }
685
- if (activationsResult.success) {
686
- setRuleCodeItems(activationsResult.data.activations.filter((item) => item.channel === "code_tool_hook" && item.status === "active"));
687
- }
688
- else {
689
- setRuleCodeItems([]);
690
- setRuleCodeErrorReason(activationsResult.error);
691
- }
692
- // PRI-629: 统一 Owner Inbox (decision-capable NHR + approvals + rulecode)。
693
- // 投影失败不阻塞页面 (rc-9 降级 — 显示原因,治理摘要仍可用)。
694
- const decisionsResult = await fetchOwnerDecisions();
695
- if (decisionsResult.success) {
696
- setOwnerDecisionItems(decisionsResult.data.items);
697
- setOwnerDecisionError(null);
698
- }
699
- else {
700
- setOwnerDecisionItems([]);
701
- setOwnerDecisionError(decisionsResult.error ?? "Owner decisions unavailable");
702
- }
703
- // Owner Decision Experience v1 inbox: failure degrades observably (rc-9),
704
- // never blocking the rest of the focus page.
705
- const ownerInboxResult = await fetchOwnerDecisionInbox();
706
- if (ownerInboxResult.success) {
707
- setOwnerInbox(ownerInboxResult.data);
708
- setOwnerInboxError(null);
709
- }
710
- else {
711
- setOwnerInbox(null);
712
- setOwnerInboxError(ownerInboxResult.reason === 'feature_disabled' ? null : (ownerInboxResult.error ?? "Owner decision inbox unavailable"));
713
- }
714
- setLoadingState("loaded");
360
+ await Promise.all([
361
+ experienceMode
362
+ ? settle('summary', fetchGovernanceExperience(), result => {
363
+ if (result.success)
364
+ setExperienceData(result.data);
365
+ else
366
+ setErrorMessage(result.error);
367
+ }, setErrorMessage)
368
+ : settle('summary', fetchGovernanceQueue(), result => {
369
+ if (result.success)
370
+ setQueueData(result.data);
371
+ else
372
+ setErrorMessage(result.error);
373
+ }, setErrorMessage),
374
+ settle('approvals', fetchApprovalsGrouped(promptTargetHost ?? undefined), result => {
375
+ if (!result.success) {
376
+ setGroupedErrorReason(result.error);
377
+ return;
378
+ }
379
+ const data = validateApprovalsGroupedData(result.data);
380
+ setGroupedData(data);
381
+ if (data === null)
382
+ setGroupedErrorReason(t('pages.focus.workbench.approvalsUnavailable'));
383
+ }, setGroupedErrorReason),
384
+ settle('activations', fetchAllActivations(promptTargetHost ?? undefined), result => {
385
+ if (result.success)
386
+ setRuleCodeItems(result.data.activations.filter(item => item.channel === 'code_tool_hook' && item.status === 'active'));
387
+ else
388
+ setRuleCodeErrorReason(result.error);
389
+ }, setRuleCodeErrorReason),
390
+ settle('bindings', fetchRuleBindingQueue(), result => {
391
+ if (result.success)
392
+ setBindingQueue(result.data);
393
+ else
394
+ setBindingError(result.error);
395
+ }, setBindingError),
396
+ settle('decisions', fetchOwnerDecisions(), result => {
397
+ if (result.success)
398
+ setOwnerDecisionItems(result.data.items);
399
+ else
400
+ setOwnerDecisionError(result.error);
401
+ }, setOwnerDecisionError),
402
+ settle('inbox', fetchOwnerDecisionInbox(), result => {
403
+ if (result.success)
404
+ setOwnerInbox(result.data);
405
+ else if (result.reason !== 'feature_disabled')
406
+ setOwnerInboxError(result.error);
407
+ }, setOwnerInboxError),
408
+ ]);
715
409
  }, [experienceMode, t, promptTargetHost]);
410
+ function changePromptTargetHost(host) {
411
+ if (host === promptTargetHost)
412
+ return;
413
+ requestEpoch.current++;
414
+ setPendingSources(FOCUS_SOURCES);
415
+ setCompletedMessage(null);
416
+ setPromptTargetHost(host);
417
+ }
716
418
  useEffect(() => {
717
419
  if (!flagsResolved)
718
420
  return;
719
- loadData();
421
+ void loadData();
422
+ return () => { requestEpoch.current++; };
720
423
  }, [flagsResolved, loadData]);
721
424
  // ── Loading state ────────────────────────────────────────────────────────
722
425
  if (loadingState === "loading") {
@@ -730,9 +433,6 @@ export function FocusPage({ featureFlags }) {
730
433
  const pendingGroups = groupedData?.groups.filter((g) => g.status === "pending") ?? [];
731
434
  const groupedAvailable = groupedData !== null;
732
435
  const decisionItems = selectDecisionSurfaceItems(ownerDecisionItems, groupedAvailable);
733
- // Single source for "the inline approval cards are on screen" — the section
734
- // count and the empty state must agree on it.
735
- const showApprovalCards = groupedAvailable && pendingGroups.length > 0;
736
436
  const pendingCount = queueData?.pendingReviewCount ?? 0;
737
437
  const deviationCount = queueData?.behaviorDeviationCount ?? 0;
738
438
  const stagnationSignals = queueData?.stagnationSignals ?? [];
@@ -748,14 +448,16 @@ export function FocusPage({ featureFlags }) {
748
448
  const ruleCodePending = ruleCodeItems.filter(item => item.action === 'code_tool_hook_shadow_activate' && item.enforcement !== 'safety_isolated');
749
449
  const ruleCodeAlerts = ruleCodeItems.filter(item => item.enforcement === 'safety_isolated');
750
450
  // PRI-787: 决策动作被治理就绪门锁定时,必须在卡片上给出可见原因(rc-9 —
751
- // 禁用不允许是无声的)。experienceData 未加载(null)时门默认放行、无锁定。
752
- const ownerActionsLockedReason = experienceData === null
753
- || (experienceData.readiness.authenticationMode === "authenticated"
754
- && experienceData.readiness.ownerIdentityConfiguration === "configured")
755
- ? null
756
- : t(experienceData.readiness.authenticationMode !== "authenticated"
757
- ? "pages.focus.ownerDecision.actionsLockedAuth"
758
- : "pages.focus.ownerDecision.actionsLockedIdentity");
451
+ // 未知就绪状态或新宿主的材料仍在读取时,允许阅读,不放行动作。
452
+ const ownerActionsLockedReason = pendingSources.includes('summary') || (experienceMode && experienceData === null)
453
+ ? t("pages.focus.workbench.permissionsUnknown")
454
+ : experienceData === null
455
+ || (experienceData.readiness.authenticationMode === "authenticated"
456
+ && experienceData.readiness.ownerIdentityConfiguration === "configured")
457
+ ? null
458
+ : t(experienceData.readiness.authenticationMode !== "authenticated"
459
+ ? "pages.focus.ownerDecision.actionsLockedAuth"
460
+ : "pages.focus.ownerDecision.actionsLockedIdentity");
759
461
  // ── SPEC-PD-UIUX-001 R2: one primary action surface per decision object ──
760
462
  // Matching uses the verified principleId association field (never titles).
761
463
  // See partitionInboxForDisplay for the semantics.
@@ -775,21 +477,9 @@ export function FocusPage({ featureFlags }) {
775
477
  const unmatchedInboxDecisions = inboxPartition?.unmatchedDecisions ?? [];
776
478
  // 待处理口径 (INV-01 lineage): decision + blocked-individual + recovery —
777
479
  // same three groups the inbox renders; historical unknowns stay explanatory.
778
- const inboxPendingTotal = inboxPartition?.pendingTotal ?? 0;
779
- // 可操作口径: the action cards actually on screen (decision cards + approval
780
- // cards) — the same number the「需要你决定」section header shows.
781
- const actionableCount = (ownerDecisionError === null ? decisionItems.length : 0)
782
- + (showApprovalCards ? pendingGroups.length : 0);
783
480
  // Review round 2 (P2): the headline never claims allClear from the inbox
784
- // alone, degraded payloads surface their reason, and the total is the
785
- // association-deduped union of on-screen cards and inbox items — see
786
- // deriveConclusionDisplay.
787
- const conclusionDisplay = deriveConclusionDisplay({
788
- ownerInbox,
789
- ownerInboxError,
790
- partition: inboxPartition,
791
- actionableCount,
792
- });
481
+ // alone; degraded payloads surface their reason — see deriveConclusionDisplay.
482
+ const conclusionDisplay = deriveConclusionDisplay({ ownerInbox, ownerInboxError });
793
483
  // Map codes to i18n text
794
484
  // Governance Recovery Actions v1: tasks_need_human_review interpolates the
795
485
  // needs_human_review task count, not the approvals count.
@@ -797,7 +487,27 @@ export function FocusPage({ featureFlags }) {
797
487
  const stateReasonCount = stateReasonCode === "tasks_need_human_review" ? humanReviewCount : pendingCount;
798
488
  const stateReason = getStateReasonText(stateReasonCode, t, stateReasonCount);
799
489
  const nextAction = getNextActionText(nextActionCode, t);
800
- return (_jsx(PageShell, { children: _jsxs("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: [_jsx("div", { className: "font-mono text-[12px] tracking-[0.14em] text-ink-3 uppercase mb-3", children: t("pages.focus.eyebrow") }), _jsx(ShinyText, { as: "h1", className: "text-[29px] font-semibold tracking-tight text-ink mb-2", duration: 4.5, brightness: 0.5, disabled: pendingCount === 0, children: t("pages.focus.title") }), _jsx("p", { className: "text-ink-3 text-[14px] max-w-[760px] leading-relaxed mb-7", children: t("pages.focus.subtitle") }), _jsxs("section", { "aria-label": t("pages.focus.conclusion.sectionTitle"), "data-testid": "focus-conclusion", className: "mb-7 px-[18px] py-[14px] bg-panel border border-line rounded-[6px]", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("span", { className: `inline-flex items-center rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase ${experienceMode && experienceData !== null
490
+ const sourcesUncertain = pendingSources.length > 0 || conclusionDisplay.mode === 'degraded' || experienceData?.dataQuality.degraded === true
491
+ || ruleCodeErrorReason !== null || errorMessage !== null || ownerDecisionError !== null || groupedErrorReason !== null || bindingError !== null || ownerInboxError !== null;
492
+ const workItems = [
493
+ ...ruleCodePending.filter(item => !decisionItems.some(decision => decision.kind === 'rulecode_decision' && decision.taskId === item.activationId)).map(item => ({ key: `activation:${item.activationId}`, title: splitReadingTitleBody(t('pages.focus.workbench.rulePromotion')).title,
494
+ kind: 'promotion', summary: t('pages.focus.workbench.promotionSummary'), reference: item.activationId,
495
+ content: _jsxs("div", { className: "text-sm leading-relaxed text-ink-2", children: [_jsx("p", { children: t('pages.focus.workbench.promotionSummary') }), _jsx(Link, { to: `/activation?activationId=${encodeURIComponent(item.activationId)}`, className: "mt-4 inline-block text-gov underline", children: t('pages.focus.workbench.openActivation') })] }) })),
496
+ ...decisionItems.map(item => ({ key: `decision:${item.reviewKey}`, title: splitReadingTitleBody(item.title).title,
497
+ kind: decisionWorkKind(item.kind), summary: item.summary, reference: item.taskId,
498
+ content: _jsx(OwnerDecisionCard, { item: item, governanceReady: ownerActionsLockedReason === null, actionsLockedReason: ownerActionsLockedReason ?? undefined, onResolved: () => { setCompletedMessage({ key: `decision:${item.reviewKey}`, message: t('pages.focus.workbench.decisionRecorded') }); void loadData(); } }, `${item.reviewKey}:${promptTargetHost ?? 'default'}`) })),
499
+ ...pendingGroups.flatMap(group => group.records.filter(record => record.status === 'pending').map((record, index) => ({
500
+ key: `approval:${record.id}`, title: new Set(group.records.map(entry => entry.artifactId)).size > 1 ? t('pages.focus.workbench.versionNeedsReview', { index: index + 1 }) : splitReadingTitleBody(selectApprovalGroupDisplayTitle(group) ?? t('pages.focus.untitledCandidate')).title,
501
+ kind: approvalWorkKind(record.channel),
502
+ summary: t('pages.focus.workbench.approvalSummary'),
503
+ content: _jsx(ApprovalSubjectReading, { principleId: group.principleId, approvalId: record.id, artifactId: record.artifactId, channel: record.channel, lockedReason: ownerActionsLockedReason ?? undefined, targetHost: promptTargetHost ?? undefined, revisionReview: new Set(group.records.filter(entry => entry.status === 'pending').map(entry => entry.artifactId)).size === 1 ? group.revisionReview : undefined, revisionReviewUnavailable: record.revisionReviewUnavailable ?? (group.records.some(entry => entry.status === 'pending' && entry.revisionReviewUnavailable === true) ? false : group.revisionReviewUnavailable), sourceUnlinked: showsUnlinkedCandidateNote(group), promptInjection: groupedData?.promptInjection, budgetNotice: groupedData?.promptInjection?.truncated === true && groupedData.promptInjection.capacityStatus !== 'unconfirmed' ? t(groupedData.promptInjection.productionRotates === true ? group.fitsPromptBudget === true ? group.promptBudgetTierFit === 'model_dependent'
504
+ ? 'pages.focus.approveQueueBadgeRotationModelDependent' : 'pages.focus.approveQueueBadgeRotation' : 'pages.focus.approveQueueBadgeOversized' : 'pages.focus.approveQueueBadge', { used: groupedData.promptInjection.usedChars, budget: groupedData.promptInjection.budget }) : undefined, onResolved: message => { setCompletedMessage({ key: `approval:${record.id}`, message }); void loadData(); } }, `${record.id}:${promptTargetHost ?? 'default'}`),
505
+ }))),
506
+ ...(bindingQueue?.items ?? []).filter(item => !['bound_generating', 'bound_advanced', 'bound_not_built'].includes(deriveRuleBindingCardState(item))).map(item => ({ key: `binding:${item.candidateId}`, title: splitReadingTitleBody(item.text).title,
507
+ kind: 'binding', summary: item.pain.text ?? t('pages.focus.workbench.bindingSummary'), reference: item.candidateId,
508
+ content: _jsx(RuleCandidateBindingPanel, { candidateId: item.candidateId, onChanged: () => { void loadData(); } }, item.candidateId) })),
509
+ ];
510
+ return (_jsx(PageShell, { className: "max-w-[1120px] owner-workbench-container", children: _jsxs("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: [_jsx("div", { className: "font-mono text-[12px] tracking-[0.14em] text-ink-3 uppercase mb-3", children: t("pages.focus.eyebrow") }), _jsx(ShinyText, { as: "h1", className: "text-[29px] font-semibold tracking-tight text-ink mb-2", duration: 4.5, brightness: 0.5, disabled: pendingCount === 0, children: t('pages.focus.workbench.title') }), _jsx("p", { className: "text-ink-3 text-[14px] max-w-[760px] leading-relaxed mb-7", children: t('pages.focus.workbench.subtitle') }), _jsxs("section", { "aria-label": t("pages.focus.conclusion.sectionTitle"), "data-testid": "focus-conclusion", className: "mb-7 px-[18px] py-[14px] bg-panel border border-line rounded-[6px]", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("span", { className: `inline-flex items-center rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase ${experienceMode && experienceData !== null
801
511
  ? EXPERIENCE_ATTENTION[experienceData.summary.primaryAttention].badgeClass
802
512
  : governanceState === "owner_review_ready"
803
513
  ? "bg-gov/10 text-gov border border-gov/20"
@@ -805,12 +515,9 @@ export function FocusPage({ featureFlags }) {
805
515
  ? "bg-amber/10 text-amber border border-amber/20"
806
516
  : governanceState === "in_progress"
807
517
  ? "bg-green/10 text-green border border-green/20"
808
- : "text-ink-4 border border-line bg-surface/80"}`, role: "status", "data-testid": "focus-conclusion-badge", children: experienceMode && experienceData !== null
518
+ : "text-ink-4 border border-line bg-surface/80"}`, role: "status", "data-testid": "focus-conclusion-badge", children: sourcesUncertain ? t('pages.focus.workbench.sourcesPending') : workItems.length > 0 ? t('pages.focus.workbench.title') : experienceMode && experienceData !== null
809
519
  ? t(EXPERIENCE_ATTENTION[experienceData.summary.primaryAttention].labelKey)
810
- : t(`pages.focus.stateLabel.${governanceState}`) }), _jsx("div", { className: "text-ink-2 text-[13px] leading-relaxed", "data-testid": "focus-conclusion-counts", children: conclusionDisplay.mode === 'ok' && conclusionDisplay.displayTotal === 0 ? (_jsx("span", { children: t("pages.focus.conclusion.allClear") })) : conclusionDisplay.mode === 'unavailable' || conclusionDisplay.mode === 'degraded' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-medium", "data-testid": "focus-conclusion-uncertain", children: t("pages.focus.conclusion.pendingUncertain") }), conclusionDisplay.actionableCount > 0 && (_jsxs("span", { className: "text-ink-4", children: [" · ", t(resolveConclusionActionableKey(conclusionDisplay.mode, ownerActionsLockedReason !== null), { count: conclusionDisplay.actionableCount })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-medium", children: t("pages.focus.conclusion.pendingTotal", { count: conclusionDisplay.displayTotal }) }), _jsxs("span", { className: "text-ink-4", children: [" · ", t(resolveConclusionActionableKey(conclusionDisplay.mode, ownerActionsLockedReason !== null), { count: conclusionDisplay.actionableCount })] })] })) })] }), conclusionDisplay.mode === 'degraded' && conclusionDisplay.degraded && (_jsxs("div", { className: "mt-3 border border-amber/30 rounded-[6px] bg-surface px-3 py-2.5", "data-testid": "focus-conclusion-degraded", children: [_jsxs("p", { className: "text-[12px] text-ink-3 leading-relaxed", children: [t("pages.focus.conclusion.degradedLabel"), " ", conclusionDisplay.degraded.reason] }), _jsx("p", { className: "mt-1 text-[12px] text-ink-3 leading-relaxed", children: conclusionDisplay.degraded.nextAction })] })), ownerActionsLockedReason !== null && (_jsxs("div", { id: "focus-global-lock-notice", className: "mt-3 flex items-start justify-between gap-3 border border-amber/30 rounded-[6px] bg-surface px-3 py-2.5", "data-testid": "focus-conclusion-lock", children: [_jsx("p", { className: "text-[12px] text-ink-3 leading-relaxed", children: ownerActionsLockedReason }), _jsx(Link, { to: "/settings", className: "shrink-0 text-[12px] text-gov underline hover:text-gov-2", children: t("pages.focus.ownerDecision.goSettingsCta") })] })), _jsx("p", { className: "mt-2 text-ink-4 text-[11.5px] leading-relaxed", "data-testid": "focus-conclusion-scope-note", children: t("pages.focus.conclusion.countsNote") })] }), _jsx(RuleCandidateBindingPanel, {}), _jsxs("section", { "aria-label": t("pages.focus.ownerDecision.sectionTitle"), className: "mb-7", "data-testid": "owner-decisions-section", children: [_jsxs("div", { className: "flex items-baseline gap-2 mb-3", children: [_jsx(SectionTitle, { children: t("pages.focus.ownerDecision.sectionTitle") }), _jsx("span", { className: "font-mono text-ink-4 text-[12px]", children: ownerDecisionError !== null
811
- ? t("pages.focus.ownerDecision.unavailable")
812
- : `· ${decisionItems.length + (showApprovalCards ? pendingGroups.length : 0)}` })] }), ownerDecisionError !== null && (_jsx("p", { className: "text-ink-4 text-[12.5px]", children: t("pages.focus.ownerDecision.loadError") })), ownerDecisionError === null && decisionItems.length === 0 && !showApprovalCards && (_jsx("p", { className: "text-ink-4 text-[13px]", children: t("pages.focus.ownerDecision.empty") })), decisionItems.map((item) => (_jsx(OwnerDecisionCard, { item: item, governanceReady: experienceData === null || (experienceData.readiness.authenticationMode === "authenticated"
813
- && experienceData.readiness.ownerIdentityConfiguration === "configured"), actionsLockedReason: ownerActionsLockedReason ?? undefined, onResolved: () => { void loadData(); } }, item.reviewKey))), (() => {
520
+ : t(`pages.focus.stateLabel.${governanceState}`) }), _jsxs("div", { className: "text-ink-2 text-[13px] leading-relaxed", "data-testid": "focus-conclusion-counts", children: [_jsx("span", { className: "font-medium", children: t('pages.focus.workbench.count', { count: workItems.length }) }), sourcesUncertain && _jsx("span", { className: "ml-2 text-amber", "data-testid": "focus-conclusion-uncertain", children: t('pages.focus.workbench.uncertain') })] })] }), conclusionDisplay.mode === 'degraded' && conclusionDisplay.degraded && (_jsxs("div", { className: "mt-3 border border-amber/30 rounded-[6px] bg-surface px-3 py-2.5", "data-testid": "focus-conclusion-degraded", children: [_jsxs("p", { className: "text-[12px] text-ink-3 leading-relaxed", children: [t("pages.focus.conclusion.degradedLabel"), " ", conclusionDisplay.degraded.reason] }), _jsx("p", { className: "mt-1 text-[12px] text-ink-3 leading-relaxed", children: conclusionDisplay.degraded.nextAction })] })), ownerActionsLockedReason !== null && (_jsxs("div", { id: "focus-global-lock-notice", className: "mt-3 flex items-start justify-between gap-3 border border-amber/30 rounded-[6px] bg-surface px-3 py-2.5", "data-testid": "focus-conclusion-lock", children: [_jsx("p", { className: "text-[12px] text-ink-3 leading-relaxed", children: ownerActionsLockedReason }), _jsx(Link, { to: "/settings", className: "shrink-0 text-[12px] text-gov underline hover:text-gov-2", children: t("pages.focus.ownerDecision.goSettingsCta") })] })), _jsxs("details", { className: "mt-2 text-ink-4 text-[11.5px] leading-relaxed", children: [_jsx("summary", { className: "cursor-pointer", children: t('pages.focus.workbench.countExplanation') }), _jsx("p", { className: "mt-2", "data-testid": "focus-conclusion-scope-note", children: t("pages.focus.conclusion.countsNote") })] })] }), (errorMessage || bindingError) && _jsxs("div", { role: "alert", className: "mb-5 border-l-2 border-amber pl-3 text-sm leading-relaxed text-ink-3", children: [_jsx("p", { children: t('pages.focus.workbench.uncertain') }), _jsx("p", { className: "mt-1 text-[12px]", children: errorMessage ?? bindingError }), _jsx("button", { type: "button", onClick: () => { void loadData(); }, className: "mt-2 text-gov underline", children: t('pages.focus.workbench.retry') })] }), ruleCodeAlerts.length > 0 && _jsxs("section", { role: "alert", className: "mb-5 border-l-2 border-danger pl-3 text-sm text-ink", children: [_jsx("p", { children: t('pages.focus.workbench.safetyAlert', { count: ruleCodeAlerts.length }) }), _jsx(Link, { to: "/activation", className: "mt-2 inline-block text-gov underline", children: t('pages.focus.workbench.openActivation') })] }), groupedErrorReason && _jsx("p", { role: "alert", className: "mb-4 text-[13px] text-amber", children: t('pages.focus.workbench.approvalsUnavailable') }), _jsx(OwnerWorkbench, { items: workItems, completed: completedMessage, uncertain: sourcesUncertain }), _jsxs("section", { "aria-label": t("pages.focus.ownerDecision.sectionTitle"), className: "mb-7", "data-testid": "owner-decisions-section", children: [ownerDecisionError !== null && _jsx("p", { role: "alert", className: "text-amber text-[13px]", children: t('pages.focus.ownerDecision.loadError') }), (() => {
814
521
  const unconfirmed = groupedData?.promptInjection;
815
522
  if (unconfirmed === undefined || unconfirmed.capacityStatus !== "unconfirmed")
816
523
  return null;
@@ -842,15 +549,15 @@ export function FocusPage({ featureFlags }) {
842
549
  : "",
843
550
  }), entry.budgetFallbackReason !== undefined && (_jsxs("span", { "data-testid": `prompt-capacity-perhost-reason-${entry.hostKind}`, children: [" · ", t(`pages.focus.budgetFallbackReason.${entry.budgetFallbackReason}`, {
844
551
  defaultValue: entry.budgetFallbackReason,
845
- })] }))] }, entry.hostKind))) }), unconfirmed.nextAction !== undefined && (_jsx("p", { className: "mt-1 text-ink-2", children: unconfirmed.nextAction })), _jsxs("div", { className: "mt-2 flex gap-2", children: [["openclaw", "codex", "workbuddy"].map((host) => (_jsx("button", { type: "button", onClick: () => setPromptTargetHost(host), "data-testid": `prompt-capacity-pick-${host}`, className: `inline-flex items-center border rounded-[3px] px-[10px] py-[4px] transition-colors ${promptTargetHost === host ? "border-amber bg-amber/10" : "border-line hover:border-line-2"}`, children: t("pages.focus.promptCapacityViewAsHost", { host: host === "openclaw" ? "OpenClaw" : host === "workbuddy" ? "WorkBuddy" : "Codex" }) }, host))), promptTargetHost !== null && (_jsx("button", { type: "button", onClick: () => setPromptTargetHost(null), "data-testid": "prompt-capacity-pick-clear", className: "inline-flex items-center border border-line rounded-[3px] px-[10px] py-[4px] hover:border-line-2", children: t("pages.focus.promptCapacityClearHost", { defaultValue: "清除选择" }) }))] })] }));
846
- })(), groupedAvailable && pendingGroups.map((group) => (_jsx(PendingReviewCard, { group: group, actionsLockedReason: ownerActionsLockedReason ?? undefined, promptInjection: groupedData.promptInjection, promptTargetHost: promptTargetHost ?? undefined, onDecisionApplied: () => { void loadData(); } }, group.principleId)))] }), _jsxs("section", { "aria-label": t("pages.focus.ownerAttention.sectionTitle"), className: "mb-7", "data-testid": "owner-attention-section", children: [_jsxs("div", { className: "flex items-baseline gap-2 mb-3", children: [_jsx(SectionTitle, { children: t("pages.focus.ownerAttention.sectionTitle") }), ownerInbox !== null && ownerInboxError === null && (_jsx("span", { className: "font-mono text-ink-4 text-[12px]", "data-testid": "owner-attention-counts", children: `· ${t("pages.focus.ownerAttention.blockedCount", { count: ownerInbox.groups.blocked.length })} · ${t("pages.focus.ownerAttention.recoveryCount", { count: inboxRecovery.length })}` }))] }), ownerInboxError !== null && (_jsx("p", { className: "text-ink-4 text-[12.5px]", children: t("pages.focus.ownerInbox.loadError") })), ownerInbox !== null && ownerInboxError === null && (_jsxs(_Fragment, { children: [ownerInbox.groups.blocked.length === 0
552
+ })] }))] }, entry.hostKind))) }), unconfirmed.nextAction !== undefined && (_jsx("p", { className: "mt-1 text-ink-2", children: unconfirmed.nextAction })), _jsxs("div", { className: "mt-2 flex gap-2", children: [["openclaw", "codex", "workbuddy"].map((host) => (_jsx("button", { type: "button", onClick: () => changePromptTargetHost(host), "data-testid": `prompt-capacity-pick-${host}`, className: `inline-flex items-center border rounded-[3px] px-[10px] py-[4px] transition-colors ${promptTargetHost === host ? "border-amber bg-amber/10" : "border-line hover:border-line-2"}`, children: t("pages.focus.promptCapacityViewAsHost", { host: host === "openclaw" ? "OpenClaw" : host === "workbuddy" ? "WorkBuddy" : "Codex" }) }, host))), promptTargetHost !== null && (_jsx("button", { type: "button", onClick: () => changePromptTargetHost(null), "data-testid": "prompt-capacity-pick-clear", className: "inline-flex items-center border border-line rounded-[3px] px-[10px] py-[4px] hover:border-line-2", children: t("pages.focus.promptCapacityClearHost", { defaultValue: "清除选择" }) }))] })] }));
553
+ })()] }), _jsxs("section", { "aria-label": t("pages.focus.ownerAttention.sectionTitle"), className: "mb-7", "data-testid": "owner-attention-section", children: [_jsxs("div", { className: "flex items-baseline gap-2 mb-3", children: [_jsx(SectionTitle, { children: t("pages.focus.ownerAttention.sectionTitle") }), ownerInbox !== null && ownerInboxError === null && (_jsx("span", { className: "font-mono text-ink-4 text-[12px]", "data-testid": "owner-attention-counts", children: `· ${t("pages.focus.ownerAttention.blockedCount", { count: ownerInbox.groups.blocked.length })} · ${t("pages.focus.ownerAttention.recoveryCount", { count: inboxRecovery.length })}` }))] }), ownerInboxError !== null && (_jsx("p", { className: "text-ink-4 text-[12.5px]", children: t("pages.focus.ownerInbox.loadError") })), ownerInbox !== null && ownerInboxError === null && (_jsxs(_Fragment, { children: [ownerInbox.groups.blocked.length === 0
847
554
  && inboxRecovery.length === 0
848
- && unmatchedInboxDecisions.length === 0 && (_jsx("p", { className: "text-ink-4 text-[13px]", children: t("pages.focus.ownerAttention.empty") })), unmatchedInboxDecisions.length > 0 && (_jsx("p", { className: "mb-2 text-ink-4 text-[12px] leading-relaxed", "data-testid": "owner-attention-unmatched-note", children: t("pages.focus.ownerAttention.unmatchedTitle") })), unmatchedInboxDecisions.map((item) => (_jsx(InboxItem, { item: item, t: t, note: t("pages.focus.ownerAttention.unmatchedItemNote") }, item.principleId))), inboxBlockedIndividual.map((item) => (_jsx(InboxItem, { item: item, t: t }, item.principleId))), inboxRecovery.map((item) => (_jsx(InboxItem, { item: item, t: t }, item.principleId))), inboxBlockedAggregate.length > 0 && (_jsxs("p", { className: "mt-2 text-ink-4 text-[12.5px] leading-relaxed", "data-testid": "owner-attention-aggregate-notice", children: [t("pages.focus.ownerAttention.blockedAggregate", { count: inboxBlockedAggregate.length }), " ", _jsx(Link, { to: "/principles", className: "text-gov hover:underline", children: t("pages.focus.ownerInbox.viewLibrary") })] })), ownerInbox.historicalUnknown.count > 0 && (_jsxs("p", { className: "text-ink-4 text-[12.5px] leading-relaxed mt-2", "data-testid": "owner-inbox-aggregate-notice", children: [ownerInbox.historicalUnknown.reasonText, " ", _jsx(Link, { to: "/principles", className: "text-gov hover:underline", children: t("pages.focus.ownerInbox.viewLibrary") })] }))] }))] }), experienceMode && experienceData !== null ? (_jsx(ExperienceSummaryCard, { snapshot: experienceData })) : !experienceMode ? (_jsxs("div", { className: "mb-7 px-[18px] py-[14px] bg-panel border border-line rounded-[6px]", children: [_jsx("div", { className: "flex items-center gap-2 mb-1", children: _jsx("span", { className: `inline-flex items-center rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase ${governanceState === "owner_review_ready" ? "bg-gov/10 text-gov border border-gov/20"
849
- : governanceState === "degraded" ? "bg-amber/10 text-amber border border-amber/20"
850
- : governanceState === "in_progress" ? "bg-green/10 text-green border border-green/20"
851
- : "text-ink-4 border border-line bg-surface/80"}`, role: "status", children: t(`pages.focus.stateLabel.${governanceState}`) }) }), _jsx("div", { className: "text-ink-2 text-[13px] leading-relaxed mt-1", children: stateReason }), _jsxs("div", { className: "text-ink-4 text-[13px] leading-relaxed mt-1", children: [_jsx("span", { className: "font-medium", children: t("pages.focus.nextActionLabel") }), " ", nextAction] })] })) : null, !experienceMode && (_jsxs(_Fragment, { children: [_jsx(ProseSummary, { pendingCount: pendingCount, deviationCount: deviationCount, stagnationCount: stagnationCount, evidenceCount: evidenceCount }), _jsx(FeedbackStratification, { gateBlocksToday: gateBlocksToday, inProgressCount: evidenceCount, pendingCount: pendingCount }), !experienceMode && governanceState === "none" && pendingCount === 0 && deviationCount === 0 && stagnationCount === 0 && (stateReasonCode === "state_db_missing" ? (_jsx(ZeroStateDbMissing, {})) : (_jsxs(_Fragment, { children: [_jsx(ZeroStateHealthy, {}), _jsx("div", { className: "mt-4", children: _jsx(OnboardingGuide, {}) })] }))), governanceState === "in_progress" && inProgressSummary && (_jsx(InProgressGuide, { summary: inProgressSummary })), degradedSignals && degradedSignals.length > 0 && (_jsx(DegradedSummary, { signals: degradedSignals }))] })), experienceMode && experienceData !== null && experienceData.summary.primaryAttention === "all_clear" && pendingGroups.length === 0 && (_jsx("div", { className: "mt-4", children: _jsx(OnboardingGuide, {}) })), !experienceMode && (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-deviation", children: [_jsx(SectionTitle, { id: "section-deviation", children: evidenceCount > 0
852
- ? t("pages.focus.sectionEvidenceInProgress")
853
- : t("pages.focus.sectionDeviation") }), deviationCount > 0 ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "text-ink-2 text-sm mb-3", children: [_jsx("span", { className: "font-mono font-semibold text-ink", children: deviationCount }), " ", t("pages.focus.deviationCount", { count: deviationCount })] }), _jsx("div", { className: "text-ink-4 text-[13px] bg-surface/60 border-l-2 border-amber px-3 py-2 mb-4", children: t("pages.focus.deviationDisclaimer") }), _jsxs("details", { className: "group", children: [_jsx("summary", { className: "text-gov text-[13px] cursor-pointer hover:underline focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.viewFullChain") }), _jsx("div", { className: "mt-2 pl-4 border-l-2 border-line text-ink-3 text-[13px]", children: t("pages.focus.deviationDetailPending") })] })] })) : evidenceCount > 0 ? (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.focus.evidenceInProgress", { count: evidenceCount }) })) : (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.focus.emptyDeviation") }))] })), !experienceMode && (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-signals", children: [_jsx(SectionTitle, { id: "section-signals", children: t("pages.focus.sectionSignals") }), stagnationSignals.length > 0 ? (_jsx("div", { className: "space-y-[10px]", children: stagnationSignals.map((signal) => (_jsx(StagnationSignalCard, { signal: signal }, signal.principleId))) })) : (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.focus.emptySignals") }))] })), _jsx(DailyThoughtCard, {}), _jsx("footer", { className: "mt-12 pt-6 border-t border-line text-ink-3 text-[13px]", children: t("pages.focus.footer") })] }) }));
555
+ && unmatchedInboxDecisions.length === 0 && (_jsx("p", { className: "text-ink-4 text-[13px]", children: t("pages.focus.ownerAttention.empty") })), unmatchedInboxDecisions.length > 0 && (_jsx("p", { className: "mb-2 text-ink-4 text-[12px] leading-relaxed", "data-testid": "owner-attention-unmatched-note", children: t("pages.focus.ownerAttention.unmatchedTitle") })), unmatchedInboxDecisions.map((item) => (_jsx(InboxItem, { item: item, t: t, note: t("pages.focus.ownerAttention.unmatchedItemNote") }, item.principleId))), inboxBlockedIndividual.map((item) => (_jsx(InboxItem, { item: item, t: t }, item.principleId))), inboxRecovery.map((item) => (_jsx(InboxItem, { item: item, t: t }, item.principleId))), inboxBlockedAggregate.length > 0 && (_jsxs("p", { className: "mt-2 text-ink-4 text-[12.5px] leading-relaxed", "data-testid": "owner-attention-aggregate-notice", children: [t("pages.focus.ownerAttention.blockedAggregate", { count: inboxBlockedAggregate.length }), " ", _jsx(Link, { to: "/principles", className: "text-gov hover:underline", children: t("pages.focus.ownerInbox.viewLibrary") })] })), ownerInbox.historicalUnknown.count > 0 && (_jsxs("p", { className: "text-ink-4 text-[12.5px] leading-relaxed mt-2", "data-testid": "owner-inbox-aggregate-notice", children: [ownerInbox.historicalUnknown.reasonText, " ", _jsx(Link, { to: "/principles", className: "text-gov hover:underline", children: t("pages.focus.ownerInbox.viewLibrary") })] }))] }))] }), _jsxs("details", { className: "mt-8 border-t border-line pt-5", "data-testid": "focus-runtime-records", children: [_jsx("summary", { className: "cursor-pointer text-[14px] text-ink-3", children: t('pages.focus.workbench.runtimeRecords') }), _jsxs("div", { className: "mt-5", children: [_jsxs("details", { className: "mb-6", children: [_jsx("summary", { className: "cursor-pointer text-sm text-gov", children: t('pages.focus.ruleBinding.title') }), _jsx(RuleCandidateBindingPanel, { onChanged: () => { void loadData(); } })] }), experienceMode && experienceData !== null && !(experienceData.summary.primaryAttention === 'all_clear' && (sourcesUncertain || workItems.length > 0)) ? (_jsx(ExperienceSummaryCard, { snapshot: experienceData })) : !experienceMode ? (_jsxs("div", { className: "mb-7 px-[18px] py-[14px] bg-panel border border-line rounded-[6px]", children: [_jsx("div", { className: "flex items-center gap-2 mb-1", children: _jsx("span", { className: `inline-flex items-center rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase ${governanceState === "owner_review_ready" ? "bg-gov/10 text-gov border border-gov/20"
556
+ : governanceState === "degraded" ? "bg-amber/10 text-amber border border-amber/20"
557
+ : governanceState === "in_progress" ? "bg-green/10 text-green border border-green/20"
558
+ : "text-ink-4 border border-line bg-surface/80"}`, role: "status", children: t(`pages.focus.stateLabel.${governanceState}`) }) }), _jsx("div", { className: "text-ink-2 text-[13px] leading-relaxed mt-1", children: stateReason }), _jsxs("div", { className: "text-ink-4 text-[13px] leading-relaxed mt-1", children: [_jsx("span", { className: "font-medium", children: t("pages.focus.nextActionLabel") }), " ", nextAction] })] })) : null, !experienceMode && (_jsxs(_Fragment, { children: [_jsx(ProseSummary, { pendingCount: pendingCount, deviationCount: deviationCount, stagnationCount: stagnationCount, evidenceCount: evidenceCount }), _jsx(FeedbackStratification, { gateBlocksToday: gateBlocksToday, inProgressCount: evidenceCount, pendingCount: pendingCount }), !experienceMode && governanceState === "none" && pendingCount === 0 && deviationCount === 0 && stagnationCount === 0 && (stateReasonCode === "state_db_missing" ? (_jsx(ZeroStateDbMissing, {})) : (_jsxs(_Fragment, { children: [_jsx(ZeroStateHealthy, {}), _jsx("div", { className: "mt-4", children: _jsx(OnboardingGuide, {}) })] }))), governanceState === "in_progress" && inProgressSummary && (_jsx(InProgressGuide, { summary: inProgressSummary })), degradedSignals && degradedSignals.length > 0 && (_jsx(DegradedSummary, { signals: degradedSignals }))] })), experienceMode && experienceData !== null && experienceData.summary.primaryAttention === "all_clear" && !sourcesUncertain && workItems.length === 0 && (_jsx("div", { className: "mt-4", children: _jsx(OnboardingGuide, {}) })), !experienceMode && (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-deviation", children: [_jsx(SectionTitle, { id: "section-deviation", children: evidenceCount > 0
559
+ ? t("pages.focus.sectionEvidenceInProgress")
560
+ : t("pages.focus.sectionDeviation") }), deviationCount > 0 ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "text-ink-2 text-sm mb-3", children: [_jsx("span", { className: "font-mono font-semibold text-ink", children: deviationCount }), " ", t("pages.focus.deviationCount", { count: deviationCount })] }), _jsx("div", { className: "text-ink-4 text-[13px] bg-surface/60 border-l-2 border-amber px-3 py-2 mb-4", children: t("pages.focus.deviationDisclaimer") }), _jsxs("details", { className: "group", children: [_jsx("summary", { className: "text-gov text-[13px] cursor-pointer hover:underline focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: t("pages.focus.viewFullChain") }), _jsx("div", { className: "mt-2 pl-4 border-l-2 border-line text-ink-3 text-[13px]", children: t("pages.focus.deviationDetailPending") })] })] })) : evidenceCount > 0 ? (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.focus.evidenceInProgress", { count: evidenceCount }) })) : (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.focus.emptyDeviation") }))] })), !experienceMode && (_jsxs("section", { className: "mt-8", "aria-labelledby": "section-signals", children: [_jsx(SectionTitle, { id: "section-signals", children: t("pages.focus.sectionSignals") }), stagnationSignals.length > 0 ? (_jsx("div", { className: "space-y-[10px]", children: stagnationSignals.map((signal) => (_jsx(StagnationSignalCard, { signal: signal }, signal.principleId))) })) : (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.focus.emptySignals") }))] }))] })] }), _jsx("div", { className: "mt-8", children: _jsx(DailyThoughtCard, {}) }), _jsx("footer", { className: "mt-12 pt-6 border-t border-line text-ink-3 text-[13px]", children: t("pages.focus.footer") })] }) }));
854
561
  }
855
562
  /** Owner Decision Experience v1 inbox item — display-only (SPEC §11.1). */
856
563
  function InboxItem({ item, t, note }) {