feat(web): disable unmanaged approval actions
Hide submit/review/approve on none docs, surface the blocked-run hint in chat, and warn that unmanaged duties cannot be added after activation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,6 +59,10 @@ import {
|
||||
resolveReconnectRunId,
|
||||
shouldSubscribeExistingRun,
|
||||
} from './lib/taskRunReconnect';
|
||||
import {
|
||||
GOVERNANCE_BLOCKED_HINT,
|
||||
isBlockedGovernanceStatus,
|
||||
} from './lib/blockedGovernanceCopy';
|
||||
import {
|
||||
buildControlledLoadErrorSkeleton,
|
||||
buildControlledLoadingSkeleton,
|
||||
@@ -2815,6 +2819,19 @@ export default function App() {
|
||||
retainedRunId = created.run_id;
|
||||
taskRunStream.setRunId(sessionId, created.run_id);
|
||||
patchAsst({ taskRunId: created.run_id });
|
||||
if (isBlockedGovernanceStatus(created.status)) {
|
||||
settled = true;
|
||||
patchAsst({
|
||||
status: 'blocked_pending_governance',
|
||||
content: GOVERNANCE_BLOCKED_HINT,
|
||||
livePhaseLabel: undefined,
|
||||
taskRunId: created.run_id,
|
||||
});
|
||||
taskRunStream.setRunId(sessionId, null);
|
||||
taskRunStream.setThinking(sessionId, false);
|
||||
await chatWindow.refresh().catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
await subscribeTaskRunEvents(created.run_id, { onEvent }, controller.signal);
|
||||
return;
|
||||
}
|
||||
@@ -2844,6 +2861,18 @@ export default function App() {
|
||||
taskRunStream.setThinking(sessionId, false);
|
||||
return;
|
||||
}
|
||||
if (isBlockedGovernanceStatus(remote.status)) {
|
||||
settled = true;
|
||||
patchAsst({
|
||||
status: 'blocked_pending_governance',
|
||||
content: GOVERNANCE_BLOCKED_HINT,
|
||||
livePhaseLabel: undefined,
|
||||
taskWorkspaceId: remote.workspace_id,
|
||||
taskRunId: runId,
|
||||
});
|
||||
taskRunStream.setThinking(sessionId, false);
|
||||
return;
|
||||
}
|
||||
if (remote.status === 'completed') {
|
||||
settled = true;
|
||||
patchAsst({
|
||||
|
||||
@@ -28,6 +28,8 @@ export function mapTaskStatusToMessageStatus(
|
||||
case 'quality_failed':
|
||||
// 门禁未过必须保持失败态,不得映射 completed
|
||||
return 'quality_failed';
|
||||
case 'blocked_pending_governance':
|
||||
return 'blocked_pending_governance';
|
||||
case 'completed':
|
||||
case '':
|
||||
return 'completed';
|
||||
@@ -47,6 +49,8 @@ export function assistantTaskStatusLabel(status: AssistantMessageStatus | undefi
|
||||
return '待编制人确认';
|
||||
case 'quality_failed':
|
||||
return '质量未通过';
|
||||
case 'blocked_pending_governance':
|
||||
return '待激活工作流';
|
||||
case 'failed':
|
||||
return '执行失败';
|
||||
case 'cancelled':
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ChatErrorInfo, ChatMessage } from '../../types/workbench';
|
||||
import { assistantTaskStatusLabel } from '../../api/chatWindow';
|
||||
import { isBlockedGovernanceBubble } from '../../lib/blockedGovernanceCopy';
|
||||
import { PhaseEventStrip } from './PhaseEventStrip';
|
||||
import { RecommendedCaseCard } from './RecommendedCaseCard';
|
||||
import { InteractiveCard } from './InteractiveCard';
|
||||
@@ -62,6 +63,9 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
||||
const isFailed = !isUser && status === 'failed' && !!message.errorInfo;
|
||||
const isCancelled = !isUser && status === 'cancelled';
|
||||
const isPendingAuthorConfirmation = !isUser && status === 'pending_author_confirmation';
|
||||
const isBlockedGovernance =
|
||||
!isUser &&
|
||||
isBlockedGovernanceBubble({ taskStatus: status, content: message.content });
|
||||
const taskStatusLabel = assistantTaskStatusLabel(status);
|
||||
const hasContent = !!message.content && message.content.trim().length > 0;
|
||||
const [diagCopied, setDiagCopied] = useState(false);
|
||||
@@ -273,7 +277,7 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
||||
? 'var(--brand-500)'
|
||||
: isFailed
|
||||
? 'var(--danger-bg)'
|
||||
: isPendingAuthorConfirmation
|
||||
: isPendingAuthorConfirmation || isBlockedGovernance
|
||||
? 'rgba(245, 158, 11, 0.08)'
|
||||
: 'var(--bg-panel)',
|
||||
color: isUser ? '#ffffff' : 'var(--text-strong)',
|
||||
@@ -281,7 +285,7 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
||||
? 'none'
|
||||
: isFailed
|
||||
? '1px solid var(--danger)'
|
||||
: isPendingAuthorConfirmation
|
||||
: isPendingAuthorConfirmation || isBlockedGovernance
|
||||
? '1px solid rgba(245, 158, 11, 0.45)'
|
||||
: '1px solid var(--border)',
|
||||
boxShadow: isUser ? '0 2px 8px rgba(79, 107, 255, 0.25)' : 'var(--shadow-xs)',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DocumentAST } from '../../types/block';
|
||||
import { UserRole } from '../../types/docgov';
|
||||
import { canShowDocGovApprovalAction, isUnmanagedDocDuties } from '../../lib/docGovActionVisibility';
|
||||
import {
|
||||
Send,
|
||||
CheckCircle2,
|
||||
@@ -68,6 +69,11 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
const isPrivate = ast.category === 'private';
|
||||
const isShared = ast.category === 'shared';
|
||||
const isWorkflow = !isPrivate && !isShared;
|
||||
const duties = { authorId: ast.authorId, reviewerId: ast.reviewerId, approverId: ast.approverId };
|
||||
const unmanagedDuties = isUnmanagedDocDuties(duties);
|
||||
const showSubmit = canShowDocGovApprovalAction('submit', currentRole, duties);
|
||||
const showReview = canShowDocGovApprovalAction('review', currentRole, duties);
|
||||
const showApprove = canShowDocGovApprovalAction('approve', currentRole, duties);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -402,7 +408,11 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
{/* 1. 编制阶段 */}
|
||||
{ast.lifecycleState === 'drafting' && (
|
||||
<>
|
||||
{currentRole === 'author' ? (
|
||||
{unmanagedDuties ? (
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||
该文档未纳入人工审批,不提供送审、审核或批准
|
||||
</span>
|
||||
) : showSubmit ? (
|
||||
<>
|
||||
{!ast.isClaimed ? (
|
||||
<button
|
||||
@@ -469,7 +479,7 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
>
|
||||
<Lock size={12} /> 文档已锁定
|
||||
</span>
|
||||
{currentRole === 'reviewer' ? (
|
||||
{showReview ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openReject('review')}
|
||||
@@ -522,7 +532,7 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
{/* 3. 批准阶段 */}
|
||||
{ast.lifecycleState === 'approving' && (
|
||||
<>
|
||||
{currentRole === 'approver' ? (
|
||||
{showApprove ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => openReject('approve')}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
needsThreeMembersForDuties,
|
||||
toggleHumanApproval,
|
||||
} from '../../lib/projectSettingsDuties';
|
||||
import { ACTIVATION_UNMANAGED_FREEZE_HINT } from '../../lib/blockedGovernanceCopy';
|
||||
import type { ProjectSettingsIssue } from '../../api/projectSettingsError';
|
||||
|
||||
interface ProjectMemberOption {
|
||||
@@ -582,8 +583,8 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text-muted)', margin: 0 }}>
|
||||
确认后将锁定工作流配置,运行期间不可直接修改开关与职责。令牌有效期至{' '}
|
||||
{new Date(preview.expires_at).toLocaleTimeString()}。
|
||||
确认后将锁定工作流配置,运行期间不可直接修改开关与职责。{ACTIVATION_UNMANAGED_FREEZE_HINT}{' '}
|
||||
令牌有效期至 {new Date(preview.expires_at).toLocaleTimeString()}。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
export const INFLIGHT_CHAT_STATUSES = ['pending', 'streaming_status', 'auto_retrying'] as const;
|
||||
export const SUCCESS_TERMINAL_CHAT_STATUSES = ['completed', 'pending_author_confirmation'] as const;
|
||||
export const ERROR_TERMINAL_CHAT_STATUSES = ['failed', 'cancelled', 'quality_failed'] as const;
|
||||
export const ERROR_TERMINAL_CHAT_STATUSES = [
|
||||
'failed',
|
||||
'cancelled',
|
||||
'quality_failed',
|
||||
'blocked_pending_governance',
|
||||
] as const;
|
||||
|
||||
export const SUCCESS_TERMINAL_STATUS_RE =
|
||||
/^(completed|pending_author_confirmation)$/;
|
||||
export const ERROR_TERMINAL_STATUS_RE = /^(failed|cancelled|quality_failed)$/;
|
||||
export const ERROR_TERMINAL_STATUS_RE = /^(failed|cancelled|quality_failed|blocked_pending_governance)$/;
|
||||
export const ANY_TERMINAL_STATUS_RE =
|
||||
/^(completed|pending_author_confirmation|failed|cancelled|quality_failed)$/;
|
||||
/^(completed|pending_author_confirmation|failed|cancelled|quality_failed|blocked_pending_governance)$/;
|
||||
|
||||
export type SquadObservationFrame = {
|
||||
userPromptMatched: boolean;
|
||||
|
||||
@@ -21,7 +21,8 @@ export function isRemoteTaskTerminal(status: string | null | undefined): boolean
|
||||
return (
|
||||
status === 'completed' ||
|
||||
status === 'pending_author_confirmation' ||
|
||||
status === 'quality_failed'
|
||||
status === 'quality_failed' ||
|
||||
status === 'blocked_pending_governance'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ export type AssistantMessageStatus =
|
||||
| 'pending_author_confirmation'
|
||||
| 'failed'
|
||||
| 'quality_failed'
|
||||
| 'blocked_pending_governance'
|
||||
| 'cancelled';
|
||||
|
||||
export interface ChatErrorInfo {
|
||||
|
||||
Reference in New Issue
Block a user