create-principles-disciple 1.145.34 → 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.
- package/_release/product-identity.json +2 -2
- package/console/dist/server/models/ApprovalsConsoleModel.d.ts +22 -2
- package/console/dist/server/models/ApprovalsConsoleModel.js +56 -16
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.d.ts +8 -0
- package/console/dist/server/models/ApprovalsGroupedConsoleModel.js +3 -2
- package/console/dist/server/models/approval-review-material.d.ts +15 -0
- package/console/dist/server/models/approval-review-material.js +32 -0
- package/console/dist/server/routes/approvals.js +32 -7
- package/console/dist/ui/api.d.ts +8 -2
- package/console/dist/ui/api.js +8 -4
- package/console/dist/ui/i18n/en.json +98 -7
- package/console/dist/ui/i18n/zh-CN.json +98 -7
- package/console/dist/ui/pages/focus/ApprovalSubjectReading.d.ts +24 -0
- package/console/dist/ui/pages/focus/ApprovalSubjectReading.js +133 -0
- package/console/dist/ui/pages/focus/FocusPage.d.ts +5 -26
- package/console/dist/ui/pages/focus/FocusPage.js +166 -459
- package/console/dist/ui/pages/focus/OwnerWorkbench.d.ts +19 -0
- package/console/dist/ui/pages/focus/OwnerWorkbench.js +75 -0
- package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.d.ts +4 -1
- package/console/dist/ui/pages/focus/RuleCandidateBindingPanel.js +7 -3
- package/console/dist/ui/pages/focus/focus-validation.js +31 -0
- package/console/dist/ui/pages/principles/PrincipleDetailPage.js +110 -86
- package/console/dist/ui/pages/principles/PrinciplesPage.js +39 -30
- package/console/dist/ui/utils/approval-review.d.ts +30 -0
- package/console/dist/ui/utils/approval-review.js +50 -0
- package/console/dist/ui/utils/reading-layout.d.ts +1 -0
- package/console/dist/ui/utils/reading-layout.js +1 -1
- package/console/dist/ui/utils/validators.d.ts +4 -0
- package/console/dist/ui/utils/validators.js +10 -2
- package/console/dist/web/assets/app.css +119 -11
- package/console/dist/web/assets/app.js +40 -30
- package/core/dist/runtime-v2/activation/activation-types.d.ts +12 -2
- package/core/dist/runtime-v2/activation/activation-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/activation-types.js.map +1 -1
- package/core/dist/runtime-v2/activation/approval-queue.d.ts +3 -2
- package/core/dist/runtime-v2/activation/approval-queue.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/approval-queue.js +6 -4
- package/core/dist/runtime-v2/activation/approval-queue.js.map +1 -1
- package/core/dist/runtime-v2/activation/approval-review-guard.d.ts +26 -0
- package/core/dist/runtime-v2/activation/approval-review-guard.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/approval-review-guard.js +25 -0
- package/core/dist/runtime-v2/activation/approval-review-guard.js.map +1 -0
- package/core/dist/runtime-v2/activation/memory-approval-store.d.ts +6 -2
- package/core/dist/runtime-v2/activation/memory-approval-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/memory-approval-store.js +26 -2
- package/core/dist/runtime-v2/activation/memory-approval-store.js.map +1 -1
- package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts +6 -2
- package/core/dist/runtime-v2/activation/sqlite-approval-store.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/sqlite-approval-store.js +59 -2
- package/core/dist/runtime-v2/activation/sqlite-approval-store.js.map +1 -1
- package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts +24 -0
- package/core/dist/runtime-v2/diagnostician-association-shadow.d.ts.map +1 -0
- package/core/dist/runtime-v2/diagnostician-association-shadow.js +84 -0
- package/core/dist/runtime-v2/diagnostician-association-shadow.js.map +1 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.d.ts +18 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.d.ts.map +1 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.js +47 -0
- package/core/dist/runtime-v2/diagnostician-association-validation.js.map +1 -0
- package/core/dist/runtime-v2/diagnostician-output.d.ts +123 -0
- package/core/dist/runtime-v2/diagnostician-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/diagnostician-output.js +41 -0
- package/core/dist/runtime-v2/diagnostician-output.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +2 -0
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +1 -0
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts +12 -1
- package/core/dist/runtime-v2/internalization/diag-router-runner.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/diag-router-runner.js +50 -2
- package/core/dist/runtime-v2/internalization/diag-router-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts +45 -0
- package/core/dist/runtime-v2/internalization/router-context-evidence.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/router-context-evidence.js +64 -0
- package/core/dist/runtime-v2/internalization/router-context-evidence.js.map +1 -0
- package/core/dist/runtime-v2/signal-collector/types.d.ts +8 -0
- package/core/dist/runtime-v2/signal-collector/types.d.ts.map +1 -1
- package/core/dist/runtime-v2/signal-collector/types.js.map +1 -1
- package/core/package.json +1 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/config-doctor.js +1 -1
- package/pd-cli/dist/commands/config-doctor.js.map +1 -1
- package/pd-cli/dist/services/config-doctor.d.ts +2 -0
- package/pd-cli/dist/services/config-doctor.d.ts.map +1 -1
- package/pd-cli/dist/services/config-doctor.js +1 -0
- package/pd-cli/dist/services/config-doctor.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +462 -462
- package/plugin/dist/core/signal-collector-host.d.ts +9 -0
- package/plugin/dist/core/signal-health.d.ts +6 -0
- package/plugin/dist/core/trajectory.d.ts +8 -0
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/dist/rulehost-evidence.js +6 -6
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/package.json +2 -2
|
@@ -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
|
|
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,
|
|
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
|
-
*
|
|
81
|
-
*
|
|
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
|
|
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):
|
|
90
|
-
*
|
|
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
|
|
101
|
-
return { mode: 'unavailable'
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
661
|
-
|
|
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
|
-
|
|
671
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
//
|
|
752
|
-
const ownerActionsLockedReason = experienceData === null
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
?
|
|
758
|
-
:
|
|
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
|
|
785
|
-
|
|
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
|
-
|
|
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}`) }),
|
|
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: () =>
|
|
846
|
-
})()
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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 }) {
|