diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx
index 24e60a8..5e23bb9 100644
--- a/src/components/Conversation.tsx
+++ b/src/components/Conversation.tsx
@@ -9,7 +9,7 @@ import { liveToMessage, livePhaseLabel, runEventsToTimeline, showProcessInline }
import { deriveShellPhase, isShellBusy, showLiveBubble, type ShellPhase } from '../runstate'
import type { Message, Part, HITLCard, ActivityCard, Artifact } from '../contracts'
import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui'
-import { docBridge, pendingConfirmations } from './Workbench'
+import { docBridge, openPendingConfirmation, pendingConfirmations } from './Workbench'
import { splitTimeline, mergeProcessTimeline, artifactIcon, humanSize, artifactIdsIn, markMissingArtifacts, dedupeDeliverables, foldRunErrors, processSummary, humanError } from '../messageparts'
import { ProcessTimeline } from './ProcessTimeline'
import { ConversationWork } from './ConversationWork'
@@ -569,7 +569,6 @@ function ConversationSession() {
const bgRunning = runs.filter(r => r.kind === 'subagent' && !isTerminal(r.status)).length
const unreadResults = cards.filter(card => card.work?.result && card.work.resultVersion > card.work.readVersion)
const pendingHere = pendingConfirmations(cards, currentConvId, currentProjectId)
- const pendingCardId = cards.find(card => card.conversationId === currentConvId && card.projectId === currentProjectId && card.status === 'action_required')?.id ?? ''
useEffect(() => {
if (!stickRef.current) return
@@ -731,12 +730,9 @@ function ConversationSession() {
- {!workPaneOwnsCards && pendingHere > 0 &&
- {TABS.map(t => (
- (
+ setTab(t.id)}>{t.label}
))}
- {tab === 'summary' && }
- {tab === 'materials' && }
- {tab === 'trace' && }
- {tab === 'timeline' && }
- {tab === 'operations' && }
+ {activeTab === 'summary' && }
+ {activeTab === 'materials' && }
+ {activeTab === 'trace' && }
+ {activeTab === 'timeline' && }
+ {activeTab === 'operations' && }
)
@@ -1181,32 +1188,33 @@ function EvidenceList({ pid, refs }: { pid: string; refs: RdmOperationRefs }) {
)
}
-/** 证据跳转:按需读取对象详情;服务端按当前正文权判定,无权时只给出可读拒绝,不假设有正文。 */
+/** 证据跳转:展开时才读取对象详情;服务端按当前正文权判定,无权时只给出可读拒绝,不假设有正文。 */
function EvidenceLink({ pid, evidence }: { pid: string; evidence: RdmOperationRef }) {
- const [open, setOpen] = useState(false)
+ const label = `${evidence.kind.label} ${evidence.id}`
+ return (
+ {`查看证据:${label}`}}>
+
+
+ )
+}
+
+function EvidenceDetail({ pid, evidence, label }: { pid: string; evidence: RdmOperationRef; label: string }) {
const [detail, setDetail] = useState(null)
const [err, setErr] = useState('')
const genRef = useRef(0)
useEffect(() => {
- if (!open) return
setDetail(null); setErr('')
return startLatestFetch(genRef,
() => api.rdmCommand('rdm.inspect.object', { projectId: pid, kind: evidence.kind.key, id: evidence.id }),
setDetail, setErr)
- }, [open, pid, evidence.kind.key, evidence.id])
- const label = `${evidence.kind.label} ${evidence.id}`
- return (
-
-
setOpen(!open)}>
- {open ? `收起证据:${label}` : `查看证据:${label}`}
-
- {open && (err
- ? (/权限拒绝|无权|受限/.test(err)
- ?
无权查看该证据(跨项目、无权或已撤权),仅保留元数据:{label}
- :
)
- : !detail ?
加载中…
:
)}
-
- )
+ }, [pid, evidence.kind.key, evidence.id])
+ if (err) {
+ return /权限拒绝|无权|受限/.test(err)
+ ? 无权查看该证据(跨项目、无权或已撤权),仅保留元数据:{label}
+ :
+ }
+ return !detail ? 加载中…
:
}
const refKey = (r: RdmObjectRef) => `${r.kind}:${r.id}`
@@ -1234,9 +1242,19 @@ function ImpactSnapshotView({ title, a }: { title: string; a: RdmImpactAnalysis
)
}
-/** 当时与当前影响:读取不可变快照;重算须显式触发(生成新分析快照,不覆盖历史)。 */
+/** 当时与当前影响:展开时才读取不可变快照;重算须显式触发(生成新分析快照,不覆盖历史)。 */
function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: string }) {
- const [open, setOpen] = useState(false)
+ return (
+
+ {`查看影响分析 ${analysisId}`}}>
+
+
+
+ )
+}
+
+function ImpactAnalysisDetail({ pid, analysisId }: { pid: string; analysisId: string }) {
const [snapshot, setSnapshot] = useState(null)
const [current, setCurrent] = useState(null)
const [err, setErr] = useState('')
@@ -1244,10 +1262,9 @@ function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: str
const snapRef = useRef(0)
const currentRef = useRef(0)
useEffect(() => {
- if (!open) return
setSnapshot(null); setErr('')
return startLatestFetch(snapRef, () => api.rdmImpactGet(pid, analysisId), setSnapshot, setErr)
- }, [open, pid, analysisId])
+ }, [pid, analysisId])
useEffect(() => () => { currentRef.current++ }, [pid, analysisId])
const revision = Number(snapshot?.start.revision)
@@ -1262,30 +1279,23 @@ function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: str
const diff = snapshot && current ? diffImpact(snapshot, current) : null
return (
-
-
setOpen(!open)}>
- {open ? '收起影响分析' : `查看影响分析 ${analysisId}`}
-
- {open && (
- <>
- {err &&
}
- {!snapshot && !err &&
加载中…
}
- {snapshot &&
}
- {snapshot && revision > 0 && (
-
- {busy ? '正在重算…' : '按当前受控图重算(生成新快照,不覆盖历史)'}
-
- )}
- {current &&
}
- {diff && (
-
-
当前新增:{diff.added.length ? diff.added.map(refLabel).join('、') : '无'}
-
当前不再涉及:{diff.removed.length ? diff.removed.map(refLabel).join('、') : '无'}
-
- )}
- >
+ <>
+ {err &&
}
+ {!snapshot && !err &&
加载中…
}
+ {snapshot &&
}
+ {snapshot && revision > 0 && (
+
+ {busy ? '正在重算…' : '按当前受控图重算(生成新快照,不覆盖历史)'}
+
)}
-
+ {current && }
+ {diff && (
+
+
当前新增:{diff.added.length ? diff.added.map(refLabel).join('、') : '无'}
+
当前不再涉及:{diff.removed.length ? diff.removed.map(refLabel).join('、') : '无'}
+
+ )}
+ >
)
}
diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx
index 11f75d2..f04fb10 100644
--- a/src/components/Workbench.tsx
+++ b/src/components/Workbench.tsx
@@ -34,10 +34,21 @@ const scratchInboxCallbacks = {
}
const inboxCallbacks = { ...scratchInboxCallbacks, onArchive: (c: ActivityCard) => store.cardToProject(c) }
-/** 本会话仍待用户处理的卡片数:右栏收起后由对话承接确认路径。 */
-export function pendingConfirmations(cards: ActivityCard[], conversationId: string | null | undefined, projectId: string | null | undefined): number {
- return cards.filter(card => card.status !== 'dismissed' && card.conversationId === conversationId && card.projectId === projectId
- && (card.status === 'action_required' || card.work?.decision?.state === 'open')).length
+type Scope = string | null | undefined
+
+/** 本会话仍待用户处理的卡片:计数与定位共用这一谓词。 */
+const isPendingConfirmation = (card: ActivityCard, conversationId: Scope, projectId: Scope) =>
+ card.status !== 'dismissed' && card.conversationId === conversationId && card.projectId === projectId
+ && (card.status === 'action_required' || card.work?.decision?.state === 'open')
+
+/** 本会话仍待用户处理的卡片数:右栏收起后由对话输入区提示承接确认路径。 */
+export function pendingConfirmations(cards: ActivityCard[], conversationId: Scope, projectId: Scope): number {
+ return cards.filter(card => isPendingConfirmation(card, conversationId, projectId)).length
+}
+
+/** 右栏收起或不在「工作」页签时的确认路径:经 openInbox 展开右栏、切到工作页签并定位首张待处理卡。 */
+export function openPendingConfirmation(cards: ActivityCard[], conversationId: Scope, projectId: Scope) {
+ store.openInbox(cards.find(card => isPendingConfirmation(card, conversationId, projectId))?.id)
}
/** 项目收件箱视图(CF3/D49/D51):项目级卡片 ∪ 其他会话待办(done 不聚合)。 */
@@ -79,7 +90,7 @@ export function Workbench() {
const pending = pendingCount(visible)
const unread = visible.filter(card => card.work && card.work.resultVersion > card.work.readVersion).length
const pendingHere = useStore(s => pendingConfirmations(s.cards, s.currentConvId, s.currentProjectId))
- const collapseHint = pendingHere > 0 ? `收起右栏(${pendingHere} 项待确认会在对话中继续处理)` : '收起右栏'
+ const collapseHint = pendingHere > 0 ? `收起右栏(${pendingHere} 项待确认可从对话输入区的提示回到工作栏处理)` : '收起右栏'
// 离开文件入口时收回全屏展开,避免出现「工作」铺满主区的怪态。
const switchTab = (tab: 'work' | 'files') =>
store.setUI({ rightTab: tab, previewExpanded: tab === 'files' ? store.getState().ui.previewExpanded : false })
diff --git a/src/store.ts b/src/store.ts
index ec2700c..e359d4c 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -11,7 +11,7 @@ import { canFlushOutbox, deriveShellPhase } from './runstate'
import { flashElement } from './scrollsync'
import { isThemeChoice, resolveTheme, type ThemeChoice } from './theme'
import { mergeSurfacesByRevision, surfaceKey } from './surfaceKey'
-import { cockpitReady, rdmAuditReady, rdmExportReady } from './workspaces'
+import { cockpitReady, rdmAuditOperationsReady, rdmAuditReady, rdmExportReady } from './workspaces'
import { rdmRolesReady, rdmMembersReady, rdmDutyReady, rdmPermMatrixReady } from './rdmManagement'
import { NO_RENDER_CAPABILITIES, renderCapabilities, type RenderCapabilities } from './documentRenderPresentation'
@@ -148,6 +148,8 @@ interface AppState {
rdmCockpitAvailable: boolean
/** 概览导出区:三条 rdm.inspect.export.* 都启用才为 true。不参与七命令就绪门。 */
rdmExportAvailable: boolean
+ /** 「审计记录」页签(W10):五条 W8b/W5 命令都启用才为 true。不参与七命令就绪门。 */
+ rdmAuditOperationsAvailable: boolean
/** U6 RDM 管理页能力:命令面齐备才显示对应分区/页签(普通基座零暴露,fail-closed)。 */
rdmRolesAvailable: boolean
rdmMembersAvailable: boolean
@@ -227,6 +229,7 @@ let state: AppState = {
rdmAuditAvailable: false,
rdmCockpitAvailable: false,
rdmExportAvailable: false,
+ rdmAuditOperationsAvailable: false,
rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false,
rdmRender: NO_RENDER_CAPABILITIES,
adminSections: [],
@@ -659,7 +662,8 @@ export const store = {
plan: null, todos: null, artifacts: [], surfaces: [], projectSurfaces: [],
cards: [], projectCards: [], cardActionStates: {}, workRecord: null, workReference: null, inboxScope: 'conv',
runtimeMock: false, profiles: [], pluginPanels: [], serverWorkspaces: [], adminSections: [],
- rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmRolesAvailable: false, rdmMembersAvailable: false,
+ rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmAuditOperationsAvailable: false,
+ rdmRolesAvailable: false, rdmMembersAvailable: false,
rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, rdmRender: NO_RENDER_CAPABILITIES,
connected: false, posting: false, error: null, toast: null, outbox: [], outboxDraining: false,
previewTabs: {}, previewActive: {}, previewOpenIntent: 0 })
@@ -1466,6 +1470,7 @@ export const store = {
rdmAuditAvailable: rdmAuditReady(cmds),
rdmCockpitAvailable: cockpitReady(cmds),
rdmExportAvailable: rdmExportReady(cmds),
+ rdmAuditOperationsAvailable: rdmAuditOperationsReady(cmds),
rdmRolesAvailable: rdmRolesReady(cmds),
rdmMembersAvailable: rdmMembersReady(cmds),
rdmDocReviewAvailable: rdmDutyReady(cmds),
@@ -1474,7 +1479,8 @@ export const store = {
})
} catch {
if (epoch !== identityEpoch) return
- set({ rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmRolesAvailable: false, rdmMembersAvailable: false,
+ set({ rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmAuditOperationsAvailable: false,
+ rdmRolesAvailable: false, rdmMembersAvailable: false,
rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, rdmRender: NO_RENDER_CAPABILITIES })
}
try {
diff --git a/src/workspaces.ts b/src/workspaces.ts
index 0e9f0d5..f51e1f5 100644
--- a/src/workspaces.ts
+++ b/src/workspaces.ts
@@ -54,6 +54,21 @@ export function rdmAuditReady(cmds: { id?: string; enabled?: boolean }[] | null
return RDM_AUDIT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true))
}
+/** 「审计记录」页签(W10)消费的 W8b/W5 命令:操作记录列表、受控覆盖面板、影响分析快照与重算。不并入七命令就绪门。 */
+export const RDM_AUDIT_OPERATIONS_COMMANDS = [
+ 'rdm.audit.operations',
+ 'rdm.trace.coverage',
+ 'rdm.trace.pending-recheck',
+ 'rdm.compliance.impact.get',
+ 'rdm.compliance.impact.recompute',
+] as const
+
+/** 五命令齐备且 enabled 才展示「审计记录」页签(能力缺失即不暴露,避免进入即 400)。 */
+export function rdmAuditOperationsReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean {
+ if (!Array.isArray(cmds)) return false
+ return RDM_AUDIT_OPERATIONS_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true))
+}
+
/** 文档驾驶舱消费的全部 rdm.cockpit.* 命令(I1 只读面;rollout I2)。 */
export const COCKPIT_COMMANDS = [
'rdm.cockpit.overview',