@stigmer/react 3.0.9-dev.20260616060535 → 3.1.0
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/execution/ApprovalCard.d.ts +47 -3
- package/execution/ApprovalCard.d.ts.map +1 -1
- package/execution/ApprovalCard.js +138 -70
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ApprovalContext.d.ts +72 -0
- package/execution/ApprovalContext.d.ts.map +1 -0
- package/execution/ApprovalContext.js +41 -0
- package/execution/ApprovalContext.js.map +1 -0
- package/execution/ArtifactCard.d.ts.map +1 -1
- package/execution/ArtifactCard.js +3 -1
- package/execution/ArtifactCard.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +5 -3
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/EmptyChangeNotice.d.ts +38 -0
- package/execution/EmptyChangeNotice.d.ts.map +1 -0
- package/execution/EmptyChangeNotice.js +27 -0
- package/execution/EmptyChangeNotice.js.map +1 -0
- package/execution/FileChangesView.d.ts +81 -0
- package/execution/FileChangesView.d.ts.map +1 -0
- package/execution/FileChangesView.js +164 -0
- package/execution/FileChangesView.js.map +1 -0
- package/execution/FilePathLink.d.ts +24 -4
- package/execution/FilePathLink.d.ts.map +1 -1
- package/execution/FilePathLink.js +31 -9
- package/execution/FilePathLink.js.map +1 -1
- package/execution/FileReviewCard.d.ts +111 -0
- package/execution/FileReviewCard.d.ts.map +1 -0
- package/execution/FileReviewCard.js +558 -0
- package/execution/FileReviewCard.js.map +1 -0
- package/execution/FileReviewContext.d.ts +35 -0
- package/execution/FileReviewContext.d.ts.map +1 -0
- package/execution/FileReviewContext.js +23 -0
- package/execution/FileReviewContext.js.map +1 -0
- package/execution/FileReviewDock.d.ts +73 -0
- package/execution/FileReviewDock.d.ts.map +1 -0
- package/execution/FileReviewDock.js +65 -0
- package/execution/FileReviewDock.js.map +1 -0
- package/execution/McpToolDetail.d.ts.map +1 -1
- package/execution/McpToolDetail.js +38 -5
- package/execution/McpToolDetail.js.map +1 -1
- package/execution/MessageThread.d.ts +42 -3
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +263 -18
- package/execution/MessageThread.js.map +1 -1
- package/execution/PlanArtifactCard.d.ts.map +1 -1
- package/execution/PlanArtifactCard.js +3 -1
- package/execution/PlanArtifactCard.js.map +1 -1
- package/execution/PlanCompletionCard.d.ts +1 -1
- package/execution/PlanCompletionCard.js +1 -1
- package/execution/ResultView.d.ts +19 -1
- package/execution/ResultView.d.ts.map +1 -1
- package/execution/ResultView.js +141 -40
- package/execution/ResultView.js.map +1 -1
- package/execution/SubAgentSection.d.ts.map +1 -1
- package/execution/SubAgentSection.js +45 -6
- package/execution/SubAgentSection.js.map +1 -1
- package/execution/TerminalSession.d.ts +34 -0
- package/execution/TerminalSession.d.ts.map +1 -0
- package/execution/TerminalSession.js +34 -0
- package/execution/TerminalSession.js.map +1 -0
- package/execution/TodoCard.d.ts +53 -0
- package/execution/TodoCard.d.ts.map +1 -0
- package/execution/TodoCard.js +77 -0
- package/execution/TodoCard.js.map +1 -0
- package/execution/ToolArgsView.d.ts +7 -1
- package/execution/ToolArgsView.d.ts.map +1 -1
- package/execution/ToolArgsView.js +10 -9
- package/execution/ToolArgsView.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +10 -1
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +66 -19
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/ToolCallGroup.d.ts +28 -18
- package/execution/ToolCallGroup.d.ts.map +1 -1
- package/execution/ToolCallGroup.js +39 -148
- package/execution/ToolCallGroup.js.map +1 -1
- package/execution/ToolCallItem.d.ts +36 -8
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +140 -21
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/ToolRunGroup.d.ts +48 -0
- package/execution/ToolRunGroup.d.ts.map +1 -0
- package/execution/ToolRunGroup.js +168 -0
- package/execution/ToolRunGroup.js.map +1 -0
- package/execution/approval-provenance.d.ts +2 -0
- package/execution/approval-provenance.d.ts.map +1 -0
- package/execution/approval-provenance.js +6 -0
- package/execution/approval-provenance.js.map +1 -0
- package/execution/artifact-utils.d.ts +0 -10
- package/execution/artifact-utils.d.ts.map +1 -1
- package/execution/artifact-utils.js +0 -17
- package/execution/artifact-utils.js.map +1 -1
- package/execution/execution-phases.d.ts +1 -6
- package/execution/execution-phases.d.ts.map +1 -1
- package/execution/execution-phases.js +4 -14
- package/execution/execution-phases.js.map +1 -1
- package/execution/file-path-resolver.d.ts +28 -0
- package/execution/file-path-resolver.d.ts.map +1 -1
- package/execution/file-path-resolver.js +24 -0
- package/execution/file-path-resolver.js.map +1 -1
- package/execution/file-review-status.d.ts +127 -0
- package/execution/file-review-status.d.ts.map +1 -0
- package/execution/file-review-status.js +135 -0
- package/execution/file-review-status.js.map +1 -0
- package/execution/index.d.ts +39 -5
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +23 -3
- package/execution/index.js.map +1 -1
- package/execution/segment-tool-calls.d.ts +46 -0
- package/execution/segment-tool-calls.d.ts.map +1 -0
- package/execution/segment-tool-calls.js +71 -0
- package/execution/segment-tool-calls.js.map +1 -0
- package/execution/tool-categories.d.ts +62 -1
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +96 -2
- package/execution/tool-categories.js.map +1 -1
- package/execution/tool-presenter.d.ts +38 -2
- package/execution/tool-presenter.d.ts.map +1 -1
- package/execution/tool-presenter.js +23 -1
- package/execution/tool-presenter.js.map +1 -1
- package/execution/tool-rendering-primitives.d.ts.map +1 -1
- package/execution/tool-rendering-primitives.js +3 -2
- package/execution/tool-rendering-primitives.js.map +1 -1
- package/execution/useArtifactDownload.d.ts +44 -0
- package/execution/useArtifactDownload.d.ts.map +1 -0
- package/execution/useArtifactDownload.js +80 -0
- package/execution/useArtifactDownload.js.map +1 -0
- package/execution/useArtifactDownloadUrl.d.ts +67 -0
- package/execution/useArtifactDownloadUrl.d.ts.map +1 -0
- package/execution/useArtifactDownloadUrl.js +59 -0
- package/execution/useArtifactDownloadUrl.js.map +1 -0
- package/execution/useFileChangeContent.d.ts +61 -0
- package/execution/useFileChangeContent.d.ts.map +1 -0
- package/execution/useFileChangeContent.js +89 -0
- package/execution/useFileChangeContent.js.map +1 -0
- package/execution/useFileReview.d.ts +89 -0
- package/execution/useFileReview.d.ts.map +1 -0
- package/execution/useFileReview.js +106 -0
- package/execution/useFileReview.js.map +1 -0
- package/execution/useSubmitApproval.d.ts +17 -2
- package/execution/useSubmitApproval.d.ts.map +1 -1
- package/execution/useSubmitApproval.js +27 -23
- package/execution/useSubmitApproval.js.map +1 -1
- package/execution/useToolOutputContent.d.ts +44 -0
- package/execution/useToolOutputContent.d.ts.map +1 -0
- package/execution/useToolOutputContent.js +31 -0
- package/execution/useToolOutputContent.js.map +1 -0
- package/index.d.ts +6 -6
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/ApprovalPeekBar.d.ts +23 -0
- package/internal/ApprovalPeekBar.d.ts.map +1 -0
- package/internal/ApprovalPeekBar.js +25 -0
- package/internal/ApprovalPeekBar.js.map +1 -0
- package/internal/BoundedContent.d.ts +49 -0
- package/internal/BoundedContent.d.ts.map +1 -0
- package/internal/BoundedContent.js +50 -0
- package/internal/BoundedContent.js.map +1 -0
- package/internal/DecisionButton.d.ts +47 -0
- package/internal/DecisionButton.d.ts.map +1 -0
- package/internal/DecisionButton.js +42 -0
- package/internal/DecisionButton.js.map +1 -0
- package/internal/InCardDecisionError.d.ts +31 -0
- package/internal/InCardDecisionError.d.ts.map +1 -0
- package/internal/InCardDecisionError.js +23 -0
- package/internal/InCardDecisionError.js.map +1 -0
- package/internal/RevealToggle.d.ts +36 -0
- package/internal/RevealToggle.d.ts.map +1 -0
- package/internal/RevealToggle.js +32 -0
- package/internal/RevealToggle.js.map +1 -0
- package/internal/VirtualizedThread.d.ts +7 -1
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +7 -3
- package/internal/VirtualizedThread.js.map +1 -1
- package/internal/useAutoDisclosure.d.ts +34 -0
- package/internal/useAutoDisclosure.d.ts.map +1 -0
- package/internal/useAutoDisclosure.js +53 -0
- package/internal/useAutoDisclosure.js.map +1 -0
- package/internal/useElapsedSince.d.ts +30 -0
- package/internal/useElapsedSince.d.ts.map +1 -0
- package/internal/useElapsedSince.js +70 -0
- package/internal/useElapsedSince.js.map +1 -0
- package/internal/useIsOverflowing.d.ts +30 -0
- package/internal/useIsOverflowing.d.ts.map +1 -0
- package/internal/useIsOverflowing.js +47 -0
- package/internal/useIsOverflowing.js.map +1 -0
- package/internal/useIsTextTruncated.d.ts +24 -0
- package/internal/useIsTextTruncated.d.ts.map +1 -0
- package/internal/useIsTextTruncated.js +41 -0
- package/internal/useIsTextTruncated.js.map +1 -0
- package/internal/useKeyedSubmission.d.ts +49 -0
- package/internal/useKeyedSubmission.d.ts.map +1 -0
- package/internal/useKeyedSubmission.js +67 -0
- package/internal/useKeyedSubmission.js.map +1 -0
- package/package.json +4 -4
- package/session/SessionViewer.d.ts +2 -2
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +10 -4
- package/session/SessionViewer.js.map +1 -1
- package/session/index.d.ts +2 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +1 -0
- package/session/index.js.map +1 -1
- package/session/inspector/ChangesTab.d.ts +8 -5
- package/session/inspector/ChangesTab.d.ts.map +1 -1
- package/session/inspector/ChangesTab.js +11 -8
- package/session/inspector/ChangesTab.js.map +1 -1
- package/session/inspector/SessionInspector.d.ts +4 -4
- package/session/inspector/SessionInspector.d.ts.map +1 -1
- package/session/inspector/SessionInspector.js +4 -4
- package/session/inspector/SessionInspector.js.map +1 -1
- package/session/inspector/index.d.ts +0 -2
- package/session/inspector/index.d.ts.map +1 -1
- package/session/inspector/index.js +0 -1
- package/session/inspector/index.js.map +1 -1
- package/session/inspector/useSessionInspector.d.ts +8 -4
- package/session/inspector/useSessionInspector.d.ts.map +1 -1
- package/session/inspector/useSessionInspector.js +27 -20
- package/session/inspector/useSessionInspector.js.map +1 -1
- package/session/useSessionConversation.d.ts +37 -3
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +18 -2
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionFileChanges.d.ts +74 -0
- package/session/useSessionFileChanges.d.ts.map +1 -0
- package/session/useSessionFileChanges.js +178 -0
- package/session/useSessionFileChanges.js.map +1 -0
- package/src/__tests__/styles-border-layer-invariant.test.ts +105 -0
- package/src/execution/ApprovalCard.tsx +368 -230
- package/src/execution/ApprovalContext.ts +109 -0
- package/src/execution/ArtifactCard.tsx +10 -6
- package/src/execution/ArtifactPreviewModal.tsx +12 -6
- package/src/execution/EmptyChangeNotice.tsx +62 -0
- package/src/execution/FileChangesView.tsx +383 -0
- package/src/execution/FilePathLink.tsx +65 -12
- package/src/execution/FileReviewCard.tsx +1118 -0
- package/src/execution/FileReviewContext.ts +53 -0
- package/src/execution/FileReviewDock.tsx +157 -0
- package/src/execution/McpToolDetail.tsx +98 -32
- package/src/execution/MessageThread.tsx +374 -9
- package/src/execution/PlanArtifactCard.tsx +9 -6
- package/src/execution/PlanCompletionCard.tsx +1 -1
- package/src/execution/ResultView.tsx +333 -162
- package/src/execution/SubAgentSection.tsx +58 -11
- package/src/execution/TerminalSession.tsx +91 -0
- package/src/execution/TodoCard.tsx +186 -0
- package/src/execution/ToolArgsView.tsx +28 -17
- package/src/execution/ToolCallDetail.tsx +137 -26
- package/src/execution/ToolCallGroup.tsx +68 -296
- package/src/execution/ToolCallItem.tsx +265 -67
- package/src/execution/ToolRunGroup.tsx +302 -0
- package/src/execution/__tests__/ApprovalCard.test.tsx +395 -5
- package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +0 -1
- package/src/execution/__tests__/EmptyChangeNotice.test.tsx +24 -0
- package/src/execution/__tests__/FileChangesView.test.tsx +296 -0
- package/src/execution/__tests__/FilePathLink.test.tsx +92 -0
- package/src/execution/__tests__/FileReviewCard.test.tsx +1044 -0
- package/src/execution/__tests__/FileReviewDock.test.tsx +134 -0
- package/src/execution/__tests__/McpToolDetail.test.tsx +79 -0
- package/src/execution/__tests__/MessageThread.test.tsx +324 -0
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +37 -16
- package/src/execution/__tests__/ResultView.test.tsx +280 -2
- package/src/execution/__tests__/SubAgentSection.test.tsx +180 -0
- package/src/execution/__tests__/TerminalSession.test.tsx +72 -0
- package/src/execution/__tests__/TodoCard.test.tsx +102 -0
- package/src/execution/__tests__/ToolArgsView.test.tsx +48 -0
- package/src/execution/__tests__/ToolCallDetail.test.tsx +195 -0
- package/src/execution/__tests__/ToolCallGroup.test.tsx +132 -0
- package/src/execution/__tests__/ToolCallItem.test.tsx +521 -0
- package/src/execution/__tests__/ToolRunGroup.test.tsx +137 -0
- package/src/execution/__tests__/blanked-narration.test.tsx +149 -0
- package/src/execution/__tests__/buildThreadItems-approvals.test.ts +304 -0
- package/src/execution/__tests__/buildThreadItems-file-review.test.ts +331 -0
- package/src/execution/__tests__/file-path-resolver.test.ts +44 -0
- package/src/execution/__tests__/file-review-status.test.ts +343 -0
- package/src/execution/__tests__/segment-tool-calls.test.ts +124 -0
- package/src/execution/__tests__/subagent-file-review-badge.test.tsx +273 -0
- package/src/execution/__tests__/thread-keys.test.ts +195 -0
- package/src/execution/__tests__/tool-presenter.test.tsx +95 -0
- package/src/execution/__tests__/useArtifactDownload.test.tsx +67 -0
- package/src/execution/__tests__/useArtifactDownloadUrl.test.tsx +51 -0
- package/src/execution/__tests__/useFileChangeContent.test.tsx +240 -0
- package/src/execution/__tests__/useFileReview.test.tsx +236 -0
- package/src/execution/__tests__/useSubmitApproval.test.tsx +165 -0
- package/src/execution/__tests__/useToolOutputContent.test.tsx +82 -0
- package/src/execution/approval-provenance.ts +8 -0
- package/src/execution/artifact-utils.ts +0 -18
- package/src/execution/execution-phases.ts +4 -16
- package/src/execution/file-path-resolver.ts +38 -0
- package/src/execution/file-review-status.ts +219 -0
- package/src/execution/index.ts +84 -4
- package/src/execution/segment-tool-calls.ts +101 -0
- package/src/execution/tool-categories.ts +128 -1
- package/src/execution/tool-presenter.ts +58 -3
- package/src/execution/tool-rendering-primitives.tsx +11 -14
- package/src/execution/useArtifactDownload.ts +109 -0
- package/src/execution/useArtifactDownloadUrl.ts +116 -0
- package/src/execution/useFileChangeContent.ts +149 -0
- package/src/execution/useFileReview.ts +197 -0
- package/src/execution/useSubmitApproval.ts +49 -27
- package/src/execution/useToolOutputContent.ts +70 -0
- package/src/index.ts +60 -1
- package/src/internal/ApprovalPeekBar.tsx +73 -0
- package/src/internal/BoundedContent.tsx +97 -0
- package/src/internal/DecisionButton.tsx +127 -0
- package/src/internal/InCardDecisionError.tsx +47 -0
- package/src/internal/RevealToggle.tsx +91 -0
- package/src/internal/VirtualizedThread.tsx +28 -2
- package/src/internal/__tests__/ApprovalPeekBar.test.tsx +34 -0
- package/src/internal/__tests__/BoundedContent.test.tsx +92 -0
- package/src/internal/__tests__/DecisionButton.test.tsx +107 -0
- package/src/internal/__tests__/RevealToggle.test.tsx +61 -0
- package/src/internal/__tests__/useAutoDisclosure.test.ts +74 -0
- package/src/internal/__tests__/useElapsedSince.test.tsx +68 -0
- package/src/internal/__tests__/useIsOverflowing.test.ts +36 -0
- package/src/internal/__tests__/useKeyedSubmission.test.tsx +126 -0
- package/src/internal/useAutoDisclosure.ts +58 -0
- package/src/internal/useElapsedSince.ts +72 -0
- package/src/internal/useIsOverflowing.ts +61 -0
- package/src/internal/useIsTextTruncated.ts +55 -0
- package/src/internal/useKeyedSubmission.ts +101 -0
- package/src/session/SessionViewer.tsx +21 -4
- package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +198 -0
- package/src/session/__tests__/audienceWiring.test.tsx +4 -0
- package/src/session/__tests__/useSessionFileChanges.test.ts +303 -0
- package/src/session/index.ts +3 -0
- package/src/session/inspector/ChangesTab.tsx +20 -17
- package/src/session/inspector/SessionInspector.tsx +12 -10
- package/src/session/inspector/__tests__/ArtifactsTab.test.tsx +0 -1
- package/src/session/inspector/__tests__/ChangesTab.test.tsx +38 -0
- package/src/session/inspector/__tests__/useSessionInspector.test.ts +46 -61
- package/src/session/inspector/index.ts +0 -3
- package/src/session/inspector/useSessionInspector.ts +27 -20
- package/src/session/useSessionConversation.ts +80 -3
- package/src/session/useSessionFileChanges.ts +207 -0
- package/src/styles.css +35 -10
- package/src/version-history/DiffViewer.tsx +10 -7
- package/src/version-history/UnifiedDiffView.tsx +66 -0
- package/src/version-history/__tests__/DiffViewer.test.tsx +57 -0
- package/src/version-history/__tests__/UnifiedDiffView.test.tsx +50 -0
- package/src/version-history/__tests__/computeDiff.test.ts +35 -0
- package/src/version-history/__tests__/parseUnifiedDiff.test.ts +82 -0
- package/src/version-history/computeDiff.ts +6 -37
- package/src/version-history/diff-hunks.ts +63 -0
- package/src/version-history/index.ts +1 -0
- package/src/version-history/parseUnifiedDiff.ts +34 -0
- package/src/workflow/WorkflowExecutionApprovalCard.tsx +49 -24
- package/src/workflow/WorkflowExecutionTimeline.tsx +15 -4
- package/src/workflow/WorkflowExecutionTimelineEvent.tsx +8 -3
- package/src/workflow/WorkflowExecutionViewer.tsx +72 -28
- package/src/workflow/WorkflowFileReviewList.tsx +152 -0
- package/src/workflow/WorkflowTaskApprovalCard.tsx +37 -85
- package/src/workflow/__tests__/ExecutionInspector.test.tsx +30 -2
- package/src/workflow/__tests__/WorkflowExecutionApprovalCard.test.tsx +129 -0
- package/src/workflow/__tests__/WorkflowFileReviewList.test.tsx +129 -0
- package/src/workflow/__tests__/WorkflowTaskApprovalCard.test.tsx +43 -11
- package/src/workflow/__tests__/useWorkflowExecutionActions.test.tsx +276 -0
- package/src/workflow/execution-inspector/ExecutionInspector.tsx +42 -18
- package/src/workflow/index.ts +5 -0
- package/src/workflow/useWorkflowExecutionActions.ts +177 -29
- package/styles.css +1 -1
- package/version-history/DiffViewer.d.ts.map +1 -1
- package/version-history/DiffViewer.js +9 -3
- package/version-history/DiffViewer.js.map +1 -1
- package/version-history/UnifiedDiffView.d.ts +29 -0
- package/version-history/UnifiedDiffView.d.ts.map +1 -0
- package/version-history/UnifiedDiffView.js +36 -0
- package/version-history/UnifiedDiffView.js.map +1 -0
- package/version-history/computeDiff.d.ts +3 -1
- package/version-history/computeDiff.d.ts.map +1 -1
- package/version-history/computeDiff.js +5 -35
- package/version-history/computeDiff.js.map +1 -1
- package/version-history/diff-hunks.d.ts +21 -0
- package/version-history/diff-hunks.d.ts.map +1 -0
- package/version-history/diff-hunks.js +58 -0
- package/version-history/diff-hunks.js.map +1 -0
- package/version-history/index.d.ts +1 -0
- package/version-history/index.d.ts.map +1 -1
- package/version-history/index.js +1 -0
- package/version-history/index.js.map +1 -1
- package/version-history/parseUnifiedDiff.d.ts +22 -0
- package/version-history/parseUnifiedDiff.d.ts.map +1 -0
- package/version-history/parseUnifiedDiff.js +34 -0
- package/version-history/parseUnifiedDiff.js.map +1 -0
- package/workflow/WorkflowExecutionApprovalCard.d.ts +8 -1
- package/workflow/WorkflowExecutionApprovalCard.d.ts.map +1 -1
- package/workflow/WorkflowExecutionApprovalCard.js +17 -3
- package/workflow/WorkflowExecutionApprovalCard.js.map +1 -1
- package/workflow/WorkflowExecutionTimeline.d.ts +11 -2
- package/workflow/WorkflowExecutionTimeline.d.ts.map +1 -1
- package/workflow/WorkflowExecutionTimeline.js +2 -2
- package/workflow/WorkflowExecutionTimeline.js.map +1 -1
- package/workflow/WorkflowExecutionTimelineEvent.d.ts +4 -1
- package/workflow/WorkflowExecutionTimelineEvent.d.ts.map +1 -1
- package/workflow/WorkflowExecutionTimelineEvent.js +2 -2
- package/workflow/WorkflowExecutionTimelineEvent.js.map +1 -1
- package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
- package/workflow/WorkflowExecutionViewer.js +18 -10
- package/workflow/WorkflowExecutionViewer.js.map +1 -1
- package/workflow/WorkflowFileReviewList.d.ts +46 -0
- package/workflow/WorkflowFileReviewList.d.ts.map +1 -0
- package/workflow/WorkflowFileReviewList.js +48 -0
- package/workflow/WorkflowFileReviewList.js.map +1 -0
- package/workflow/WorkflowTaskApprovalCard.d.ts +8 -1
- package/workflow/WorkflowTaskApprovalCard.d.ts.map +1 -1
- package/workflow/WorkflowTaskApprovalCard.js +12 -19
- package/workflow/WorkflowTaskApprovalCard.js.map +1 -1
- package/workflow/execution-inspector/ExecutionInspector.d.ts +21 -4
- package/workflow/execution-inspector/ExecutionInspector.d.ts.map +1 -1
- package/workflow/execution-inspector/ExecutionInspector.js +6 -3
- package/workflow/execution-inspector/ExecutionInspector.js.map +1 -1
- package/workflow/index.d.ts +1 -0
- package/workflow/index.d.ts.map +1 -1
- package/workflow/index.js +1 -0
- package/workflow/index.js.map +1 -1
- package/workflow/useWorkflowExecutionActions.d.ts +62 -4
- package/workflow/useWorkflowExecutionActions.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionActions.js +94 -18
- package/workflow/useWorkflowExecutionActions.js.map +1 -1
- package/session/inspector/PlanTab.d.ts +0 -15
- package/session/inspector/PlanTab.d.ts.map +0 -1
- package/session/inspector/PlanTab.js +0 -19
- package/session/inspector/PlanTab.js.map +0 -1
- package/src/session/inspector/PlanTab.tsx +0 -31
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useEffect, useId, useMemo, useState } from "react";
|
|
4
|
+
import type {
|
|
5
|
+
CapturedFileChange,
|
|
6
|
+
FileChangeSet,
|
|
7
|
+
FileDecision,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
9
|
+
import {
|
|
10
|
+
FileCaptureClass,
|
|
11
|
+
FileChangeKind,
|
|
12
|
+
FileDecisionAction,
|
|
13
|
+
FileDecisionOrigin,
|
|
14
|
+
FileDecisionScope,
|
|
15
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
|
+
import { toDisplayFileChange } from "@stigmer/sdk";
|
|
17
|
+
import { cn } from "@stigmer/theme";
|
|
18
|
+
import { FileChangeDiff } from "./FileChangesView";
|
|
19
|
+
import { DiffSummary } from "../version-history/DiffSummary";
|
|
20
|
+
import { DecisionButton } from "../internal/DecisionButton";
|
|
21
|
+
import { InCardDecisionError } from "../internal/InCardDecisionError";
|
|
22
|
+
import { splitDisplayPath } from "./file-path-resolver";
|
|
23
|
+
import { fileDecisionKey, type FileDecisionOptions } from "./useFileReview";
|
|
24
|
+
import {
|
|
25
|
+
changeSetReviewability,
|
|
26
|
+
deriveEffectiveVerdicts,
|
|
27
|
+
fileReviewability,
|
|
28
|
+
type FileReviewability,
|
|
29
|
+
} from "./file-review-status";
|
|
30
|
+
|
|
31
|
+
/** A stable empty set so the default `submittingDecisionKeys` keeps a constant ref. */
|
|
32
|
+
const NO_KEYS: ReadonlySet<string> = new Set();
|
|
33
|
+
|
|
34
|
+
/** A stable empty map so the default `decisionErrors` keeps a constant ref. */
|
|
35
|
+
const NO_ERRORS: ReadonlyMap<string, Error> = new Map();
|
|
36
|
+
|
|
37
|
+
/** Props for {@link FileReviewCard}. */
|
|
38
|
+
export interface FileReviewCardProps {
|
|
39
|
+
/** The captured change set awaiting review (status AWAITING_REVIEW). */
|
|
40
|
+
readonly fileChangeSet: FileChangeSet;
|
|
41
|
+
/**
|
|
42
|
+
* Called when the user makes a decision. The consumer (typically
|
|
43
|
+
* {@link MessageThread} or a platform builder's surface) handles the RPC via
|
|
44
|
+
* {@link useFileReview}. A whole-set Approve/Reject sends `CHANGE_SET` scope;
|
|
45
|
+
* a per-file Keep/Discard sends `FILE` scope with the file's id. Optional and
|
|
46
|
+
* ignored when {@link FileReviewCardProps.interactive} is `false`.
|
|
47
|
+
*/
|
|
48
|
+
readonly onSubmit?: (action: FileDecisionAction, options?: FileDecisionOptions) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the card presents decision controls. Defaults to `true` (the live
|
|
51
|
+
* review surface). Set to `false` to render a **settled** change set read-only:
|
|
52
|
+
* the per-file diffs and each file's committed verdict are shown, but the
|
|
53
|
+
* decision controls, bulk footer, and error affordances are omitted. Used for a
|
|
54
|
+
* decided/reconciled set, or any set on a terminal execution — a historical
|
|
55
|
+
* record of what changed and how it was decided, not an actionable gate.
|
|
56
|
+
*/
|
|
57
|
+
readonly interactive?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the expanded body renders each file's diff. Defaults to `true` —
|
|
60
|
+
* the right default wherever this card is the *only* review surface (the
|
|
61
|
+
* workflow file-review list, standalone platform-builder embeds).
|
|
62
|
+
*
|
|
63
|
+
* `MessageThread` passes `false`: the transcript's stamped edit rows already
|
|
64
|
+
* show every diff in place, so the expanded body collapses to a compact file
|
|
65
|
+
* list (kind letter + path + per-file controls) and the card stays purely
|
|
66
|
+
* the decision surface. Decision semantics — digests, scopes,
|
|
67
|
+
* acknowledgments — are identical in both modes; this is presentation only
|
|
68
|
+
* (opt-in with a backward-compatible default, DD-011).
|
|
69
|
+
*/
|
|
70
|
+
readonly showDiffs?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The decision keys currently being submitted — exactly the set
|
|
73
|
+
* {@link useFileReview} returns as `submittingDecisionKeys`. A card has more
|
|
74
|
+
* than one decision target (the whole set plus each file), so it takes the
|
|
75
|
+
* keyed set rather than a single boolean and derives each control's in-flight
|
|
76
|
+
* state via {@link fileDecisionKey}.
|
|
77
|
+
*/
|
|
78
|
+
readonly submittingDecisionKeys?: ReadonlySet<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Per-decision failures, keyed exactly like {@link submittingDecisionKeys}
|
|
81
|
+
* (via {@link fileDecisionKey}) — the set's id for a whole-set decision, the
|
|
82
|
+
* `setId:fileChangeId` for a per-file one. A failed decision is surfaced
|
|
83
|
+
* in-card, beside the control that failed (and the optimistic verdict reverts),
|
|
84
|
+
* so the reviewer sees *which* action did not take and why. Supply
|
|
85
|
+
* {@link useFileReview}'s `decisionErrors`.
|
|
86
|
+
*/
|
|
87
|
+
readonly decisionErrors?: ReadonlyMap<string, Error>;
|
|
88
|
+
/** Additional CSS class names for the root container. */
|
|
89
|
+
readonly className?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Renders a captured {@link FileChangeSet} as a **compact decision bar**: one
|
|
94
|
+
* line carrying the set summary and the whole-set decision controls, with the
|
|
95
|
+
* per-file detail behind an expander. What the expander holds depends on
|
|
96
|
+
* {@link FileReviewCardProps.showDiffs}: the diff-rich body (default — for
|
|
97
|
+
* surfaces where this card is the only review surface) or a compact file list
|
|
98
|
+
* (the session thread, whose stamped edit rows already show every diff in
|
|
99
|
+
* place). A set that needs per-file attention (binary acknowledgment, blocked
|
|
100
|
+
* files, mid-review) starts expanded.
|
|
101
|
+
*
|
|
102
|
+
* File edits are reviewed as a captured workspace delta, not tool calls, so this
|
|
103
|
+
* is a dedicated decision surface distinct from the message transcript and the
|
|
104
|
+
* tool-approval {@link ApprovalCard}. The reviewer can decide each file
|
|
105
|
+
* individually (Keep / Discard, a `FILE`-scoped decision) or act on the whole
|
|
106
|
+
* set at once (Approve / Reject, a `CHANGE_SET` decision). Either way the runner
|
|
107
|
+
* reconciles the exact approved bytes with hash verification.
|
|
108
|
+
*
|
|
109
|
+
* The unified HITL gate clears only when EVERY file has a verdict — one
|
|
110
|
+
* `CHANGE_SET` decision, or a `FILE` decision per file — so a partially-decided
|
|
111
|
+
* set stays actionable and deciding the last file is what resumes the turn. A
|
|
112
|
+
* per-file verdict is changeable while the set is still open: clicking the other
|
|
113
|
+
* option records a new decision (the backend reconcile is last-write-wins).
|
|
114
|
+
*
|
|
115
|
+
* A `binary-only` set (binary files are the only blocker) can be kept in one
|
|
116
|
+
* action: the bulk button reads "Keep all" and carries the acknowledgment
|
|
117
|
+
* (DD-17). A `blocked` set (something unavailable to review) cannot be approved
|
|
118
|
+
* as a whole — the Approve button is disabled with an explanation, and the
|
|
119
|
+
* per-file path is the escape: keep the reviewable files and discard the rest.
|
|
120
|
+
*
|
|
121
|
+
* Each non-reviewable file is labeled honestly by {@link fileReviewability}: a
|
|
122
|
+
* `binary` change has no text diff to review; an `unavailable` change is one
|
|
123
|
+
* whose diff isn't available at all, with the honest cause the runner recorded
|
|
124
|
+
* (a secret path whose bytes were never captured, or a diff dropped to bound the
|
|
125
|
+
* status — doc 15). Both are discard-only, and their Keep affordance is disabled
|
|
126
|
+
* with the reason associated via `aria-describedby`. Files captured outside
|
|
127
|
+
* normal git tracking (gitignored / non-git CAS) carry a small provenance badge
|
|
128
|
+
* so the reviewer knows the change is not part of the repo's tracked history.
|
|
129
|
+
*
|
|
130
|
+
* Wrapped in `React.memo` — the streamed `FileChangeSet` reference is preserved
|
|
131
|
+
* by structural sharing when unchanged, so review cards skip re-renders during
|
|
132
|
+
* unrelated stream updates.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```tsx
|
|
136
|
+
* const { submitFileDecision, submittingDecisionKeys } = useFileReview();
|
|
137
|
+
* <FileReviewCard
|
|
138
|
+
* fileChangeSet={changeSet}
|
|
139
|
+
* onSubmit={(action, opts) => submitFileDecision(executionId, changeSet.id, action, opts)}
|
|
140
|
+
* submittingDecisionKeys={submittingDecisionKeys}
|
|
141
|
+
* />
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export const FileReviewCard = memo(function FileReviewCard({
|
|
145
|
+
fileChangeSet,
|
|
146
|
+
onSubmit,
|
|
147
|
+
submittingDecisionKeys = NO_KEYS,
|
|
148
|
+
decisionErrors = NO_ERRORS,
|
|
149
|
+
interactive = true,
|
|
150
|
+
showDiffs = true,
|
|
151
|
+
className,
|
|
152
|
+
}: FileReviewCardProps) {
|
|
153
|
+
const changes = fileChangeSet.changes;
|
|
154
|
+
const total = changes.length;
|
|
155
|
+
const setId = fileChangeSet.id;
|
|
156
|
+
|
|
157
|
+
// Each file's current verdict, a last-write-wins fold over the ledger-ordered
|
|
158
|
+
// FILE decisions (the same precedence the runner reconcile applies). Drives
|
|
159
|
+
// which per-file option reads as selected and the reviewed-count progress.
|
|
160
|
+
const verdictByFileId = useMemo(
|
|
161
|
+
() => deriveFileVerdicts(fileChangeSet.decisions),
|
|
162
|
+
[fileChangeSet.decisions],
|
|
163
|
+
);
|
|
164
|
+
const decidedCount = verdictByFileId.size;
|
|
165
|
+
|
|
166
|
+
// The EFFECTIVE verdicts for history (a CHANGE_SET decision covers every file
|
|
167
|
+
// a FILE decision does not override — the reconcile's own precedence), so a
|
|
168
|
+
// settled "Keep all" set reads every file as kept, never "Not reviewed".
|
|
169
|
+
// Interactive per-file selection deliberately keeps the FILE-only fold above.
|
|
170
|
+
const effectiveVerdicts = useMemo(
|
|
171
|
+
() => deriveEffectiveVerdicts(fileChangeSet),
|
|
172
|
+
[fileChangeSet],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// The set-level reviewability drives the bulk affordance. A "binary-only" set
|
|
176
|
+
// (binary files are the ONLY blocker) is keepable in one acknowledged action
|
|
177
|
+
// ("Keep all", DD-17); a "blocked" set (a secret/elided file with no keepable
|
|
178
|
+
// bytes) can never be approved at once and must be resolved per file.
|
|
179
|
+
const reviewability = changeSetReviewability(fileChangeSet);
|
|
180
|
+
const incomplete = reviewability !== "complete";
|
|
181
|
+
const binaryOnly = reviewability === "binary-only";
|
|
182
|
+
|
|
183
|
+
// Per-file controls earn their space when there is more than one file OR when a
|
|
184
|
+
// lone file is not reviewable — a single binary needs its per-file "Keep anyway"
|
|
185
|
+
// (FILE-scope, acknowledged), which the CHANGE_SET-scoped bulk controls can't
|
|
186
|
+
// offer. A single COMPLETE file stays decided once via the bar's bulk controls.
|
|
187
|
+
const showPerFile = total > 1 || incomplete;
|
|
188
|
+
// The bulk (CHANGE_SET) controls live on the bar for multi-file sets and for a
|
|
189
|
+
// single complete file; they are hidden for a single incomplete file, whose
|
|
190
|
+
// only honest decision is the per-file control in the expanded body (a single
|
|
191
|
+
// binary uses its per-file "Keep anyway" — DD-16 — and a blocked lone file can
|
|
192
|
+
// only be discarded).
|
|
193
|
+
const showBulkControls = total > 1 || !incomplete;
|
|
194
|
+
|
|
195
|
+
// The bar starts expanded exactly when the collapsed bar cannot honestly carry
|
|
196
|
+
// the decision: an incomplete set (binary acknowledgment / blocked files need
|
|
197
|
+
// their per-file context) or a review already in progress. A complete set and
|
|
198
|
+
// a settled record start collapsed — the transcript's stamped rows already
|
|
199
|
+
// show what changed, and the expander reveals the per-file detail on demand.
|
|
200
|
+
const [expanded, setExpanded] = useState(
|
|
201
|
+
() => interactive && (incomplete || decidedCount > 0),
|
|
202
|
+
);
|
|
203
|
+
const bodyId = useId();
|
|
204
|
+
|
|
205
|
+
const wholeSetSubmitting = submittingDecisionKeys.has(setId);
|
|
206
|
+
const wholeSetError = decisionErrors.get(setId) ?? null;
|
|
207
|
+
|
|
208
|
+
// Bulk (CHANGE_SET) decision state — mirrors ApprovalCardBody: one active
|
|
209
|
+
// action drives the clicked button's spinner, cleared when the RPC settles.
|
|
210
|
+
const [activeAction, setActiveAction] = useState<FileDecisionAction | null>(null);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!wholeSetSubmitting) setActiveAction(null);
|
|
213
|
+
}, [wholeSetSubmitting]);
|
|
214
|
+
|
|
215
|
+
const handleBulk = useCallback(
|
|
216
|
+
(action: FileDecisionAction) => {
|
|
217
|
+
setActiveAction(action);
|
|
218
|
+
// Whole-set decision: CHANGE_SET scope, bound to the aggregate digest the
|
|
219
|
+
// reviewer saw (the enforcement gate the runner re-verifies at reconcile).
|
|
220
|
+
// A "Keep all" on a binary-only set carries the acknowledgment: the
|
|
221
|
+
// binaries have no text diff but reconcilable bytes, so the user
|
|
222
|
+
// consciously keeps the whole set (DD-17). The server honors it only when
|
|
223
|
+
// every incompleteness is binary and never relaxes the digest gate.
|
|
224
|
+
onSubmit?.(action, {
|
|
225
|
+
scope: FileDecisionScope.CHANGE_SET,
|
|
226
|
+
expectedDigest: fileChangeSet.aggregateDigest,
|
|
227
|
+
acknowledgeUnreviewable: action === FileDecisionAction.APPROVE && binaryOnly,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
[onSubmit, fileChangeSet.aggregateDigest, binaryOnly],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const handleFileDecide = useCallback(
|
|
234
|
+
(change: CapturedFileChange, action: FileDecisionAction) => {
|
|
235
|
+
// Per-file decision: FILE scope correlated by the change id, bound to that
|
|
236
|
+
// file's digest (echoed verbatim — the server compares it against the same
|
|
237
|
+
// captured value, so it can never spuriously mismatch). A "Keep anyway" on a
|
|
238
|
+
// binary file carries the acknowledgment: it has no text diff, but its exact
|
|
239
|
+
// bytes are captured and reconcilable, so the user consciously keeps it
|
|
240
|
+
// (DD-16). The server honors this only for a binary file and never relaxes
|
|
241
|
+
// the digest gate.
|
|
242
|
+
onSubmit?.(action, {
|
|
243
|
+
scope: FileDecisionScope.FILE,
|
|
244
|
+
fileChangeId: change.id,
|
|
245
|
+
expectedDigest: change.fileDigest,
|
|
246
|
+
acknowledgeUnreviewable:
|
|
247
|
+
action === FileDecisionAction.APPROVE &&
|
|
248
|
+
fileReviewability(change).kind === "binary",
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
[onSubmit],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const totals = useMemo(() => {
|
|
255
|
+
let additions = 0;
|
|
256
|
+
let deletions = 0;
|
|
257
|
+
for (const c of changes) {
|
|
258
|
+
if (c.kind === FileChangeKind.ADD) additions += 1;
|
|
259
|
+
else if (c.kind === FileChangeKind.DELETE) deletions += 1;
|
|
260
|
+
}
|
|
261
|
+
return { additions, deletions };
|
|
262
|
+
}, [changes]);
|
|
263
|
+
|
|
264
|
+
// Which non-reviewable classes are present in the set — folded once (memoized
|
|
265
|
+
// like `totals`, DD-010) so the blocked-set copy is accurate to what's actually
|
|
266
|
+
// here (never the generic "binary or truncated").
|
|
267
|
+
const blockSummary = useMemo(() => {
|
|
268
|
+
let hasBinary = false;
|
|
269
|
+
let hasUnavailable = false;
|
|
270
|
+
for (const c of changes) {
|
|
271
|
+
const r = fileReviewability(c);
|
|
272
|
+
if (r.kind === "binary") hasBinary = true;
|
|
273
|
+
else if (r.kind === "unavailable") hasUnavailable = true;
|
|
274
|
+
}
|
|
275
|
+
return { hasBinary, hasUnavailable };
|
|
276
|
+
}, [changes]);
|
|
277
|
+
|
|
278
|
+
// Associates the disabled whole-set Approve with the notice explaining why it
|
|
279
|
+
// is unavailable (a11y: a disabled action is never an unexplained dead end).
|
|
280
|
+
const incompleteNoticeId = useId();
|
|
281
|
+
|
|
282
|
+
const labels = bulkLabels(total, decidedCount, binaryOnly);
|
|
283
|
+
// A set the DD-28 policy kept (every change produced by a command the user
|
|
284
|
+
// already approved) says so explicitly: consent was given at the command
|
|
285
|
+
// gate, and the record must never read as if a human reviewed it here.
|
|
286
|
+
const autoKept = !interactive && isPolicyAutoKept(fileChangeSet);
|
|
287
|
+
const summary = interactive
|
|
288
|
+
? `${total} file${total === 1 ? "" : "s"} awaiting review`
|
|
289
|
+
: autoKept
|
|
290
|
+
? "Kept automatically — produced by a command you approved"
|
|
291
|
+
: settledSummary(total, changes, effectiveVerdicts);
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div
|
|
295
|
+
// Interactive: an actionable alert (a pending gate). Read-only: a passive
|
|
296
|
+
// historical record, so a neutral group without the warning accent.
|
|
297
|
+
role={interactive ? "alert" : "group"}
|
|
298
|
+
aria-label={
|
|
299
|
+
interactive
|
|
300
|
+
? `Review ${total} file change${total === 1 ? "" : "s"}`
|
|
301
|
+
: `${total} file change${total === 1 ? "" : "s"}`
|
|
302
|
+
}
|
|
303
|
+
className={cn(
|
|
304
|
+
"rounded-lg border border-border-prominent",
|
|
305
|
+
interactive && "border-l-2 border-l-warning",
|
|
306
|
+
className,
|
|
307
|
+
)}
|
|
308
|
+
>
|
|
309
|
+
{/* The bar: summary + whole-set verdict + the detail expander, one line. */}
|
|
310
|
+
<div className="flex flex-wrap items-center gap-2 px-2.5 py-1.5 text-xs">
|
|
311
|
+
<span className="shrink-0 font-medium text-foreground">
|
|
312
|
+
{interactive ? "Review file changes" : "File changes"}
|
|
313
|
+
</span>
|
|
314
|
+
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
|
315
|
+
{summary}
|
|
316
|
+
</span>
|
|
317
|
+
|
|
318
|
+
{interactive && showBulkControls && (
|
|
319
|
+
<>
|
|
320
|
+
<DecisionButton
|
|
321
|
+
label={labels.approve}
|
|
322
|
+
variant="primary"
|
|
323
|
+
onClick={() => handleBulk(FileDecisionAction.APPROVE)}
|
|
324
|
+
isActive={activeAction === FileDecisionAction.APPROVE}
|
|
325
|
+
isSubmitting={wholeSetSubmitting}
|
|
326
|
+
disabled={incomplete && !binaryOnly}
|
|
327
|
+
ariaDescribedby={incomplete ? incompleteNoticeId : undefined}
|
|
328
|
+
cursorTarget="file-review-approve"
|
|
329
|
+
/>
|
|
330
|
+
<DecisionButton
|
|
331
|
+
label={labels.reject}
|
|
332
|
+
variant="danger"
|
|
333
|
+
onClick={() => handleBulk(FileDecisionAction.REJECT)}
|
|
334
|
+
isActive={activeAction === FileDecisionAction.REJECT}
|
|
335
|
+
isSubmitting={wholeSetSubmitting}
|
|
336
|
+
cursorTarget="file-review-reject"
|
|
337
|
+
/>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
<button
|
|
342
|
+
type="button"
|
|
343
|
+
aria-expanded={expanded}
|
|
344
|
+
aria-controls={bodyId}
|
|
345
|
+
onClick={() => setExpanded((v) => !v)}
|
|
346
|
+
data-cursor-target="file-review-expander"
|
|
347
|
+
className={cn(
|
|
348
|
+
"inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2 py-1",
|
|
349
|
+
"text-xs font-medium text-muted-foreground transition-colors",
|
|
350
|
+
"hover:bg-accent-hover hover:text-foreground",
|
|
351
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
352
|
+
)}
|
|
353
|
+
>
|
|
354
|
+
{/* The closed label states what expanding reveals: the reviewable
|
|
355
|
+
diffs ("Review"), the file inventory ("Files"), or a settled
|
|
356
|
+
record ("Show"). */}
|
|
357
|
+
{expanded ? "Hide" : !interactive ? "Show" : showDiffs ? "Review" : "Files"}
|
|
358
|
+
<ExpanderChevron expanded={expanded} />
|
|
359
|
+
</button>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{/* The disabled whole-set Approve's explanation must be visible even when
|
|
363
|
+
the diffs are collapsed, so it lives on the bar, not in the body. */}
|
|
364
|
+
{interactive && incomplete && showBulkControls && (
|
|
365
|
+
<p
|
|
366
|
+
id={incompleteNoticeId}
|
|
367
|
+
className="px-2.5 pb-1.5 text-[11px] italic text-muted-foreground"
|
|
368
|
+
>
|
|
369
|
+
{incompleteNotice(showPerFile, blockSummary, binaryOnly)}
|
|
370
|
+
</p>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{/* A failed whole-set decision is surfaced HERE, in-card, not via the
|
|
374
|
+
session's global error banner. A review surface has many decision
|
|
375
|
+
targets (the whole set plus each file), so a failure must name the
|
|
376
|
+
control it belongs to — something a single global banner cannot do.
|
|
377
|
+
Rendered on the bar so it is visible regardless of expansion. */}
|
|
378
|
+
{interactive && wholeSetError && (
|
|
379
|
+
<div className="px-2.5 pb-1.5">
|
|
380
|
+
<InCardDecisionError
|
|
381
|
+
error={wholeSetError}
|
|
382
|
+
leadIn="submit decision"
|
|
383
|
+
cursorTarget="file-review-error"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{expanded && (
|
|
389
|
+
<div
|
|
390
|
+
id={bodyId}
|
|
391
|
+
className="space-y-2 border-t border-border-muted px-3 py-2.5"
|
|
392
|
+
>
|
|
393
|
+
{/* The list body carries no summary header: the collapsed bar's
|
|
394
|
+
file count is the summary, and per-file counts would require
|
|
395
|
+
fetching offloaded bodies just to decorate a list. */}
|
|
396
|
+
{showDiffs && (
|
|
397
|
+
<DiffSummary
|
|
398
|
+
fileCount={total}
|
|
399
|
+
additions={totals.additions}
|
|
400
|
+
deletions={totals.deletions}
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{!interactive
|
|
405
|
+
? // Settled: every file paired with its committed EFFECTIVE
|
|
406
|
+
// verdict (a bulk decision covers files without their own), no
|
|
407
|
+
// controls — a record of what changed and how it was decided.
|
|
408
|
+
changes.map((change) => (
|
|
409
|
+
<SettledFileRow
|
|
410
|
+
key={change.id}
|
|
411
|
+
change={change}
|
|
412
|
+
verdict={effectiveVerdicts.get(change.id) ?? null}
|
|
413
|
+
showDiffs={showDiffs}
|
|
414
|
+
/>
|
|
415
|
+
))
|
|
416
|
+
: showPerFile
|
|
417
|
+
? changes.map((change) => (
|
|
418
|
+
<FileChangeReviewRow
|
|
419
|
+
key={change.id}
|
|
420
|
+
change={change}
|
|
421
|
+
verdict={verdictByFileId.get(change.id) ?? null}
|
|
422
|
+
isSubmitting={
|
|
423
|
+
submittingDecisionKeys.has(fileDecisionKey(setId, change.id)) ||
|
|
424
|
+
wholeSetSubmitting
|
|
425
|
+
}
|
|
426
|
+
error={decisionErrors.get(fileDecisionKey(setId, change.id)) ?? null}
|
|
427
|
+
onDecide={handleFileDecide}
|
|
428
|
+
showDiffs={showDiffs}
|
|
429
|
+
/>
|
|
430
|
+
))
|
|
431
|
+
: // A single complete file decided via the bar's bulk controls:
|
|
432
|
+
// its detail is informational, with no per-file control.
|
|
433
|
+
changes.map((change) =>
|
|
434
|
+
showDiffs ? (
|
|
435
|
+
<div key={change.id} className="space-y-1.5">
|
|
436
|
+
<CaptureBadge change={change} />
|
|
437
|
+
<CapturedChangeDiff change={change} />
|
|
438
|
+
</div>
|
|
439
|
+
) : (
|
|
440
|
+
<FileListRow key={change.id} change={change} />
|
|
441
|
+
),
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{interactive && showPerFile && (
|
|
445
|
+
<ReviewProgress decided={decidedCount} total={total} />
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Whether the set was kept by the approved-command auto-keep policy (DD-28):
|
|
455
|
+
* some decision carries origin POLICY_APPROVED_COMMAND. Origin is audit
|
|
456
|
+
* provenance — the record label derives from it so an automatic keep is never
|
|
457
|
+
* presented as a human review.
|
|
458
|
+
*/
|
|
459
|
+
function isPolicyAutoKept(fileChangeSet: FileChangeSet): boolean {
|
|
460
|
+
return fileChangeSet.decisions.some(
|
|
461
|
+
(d) => d.origin === FileDecisionOrigin.POLICY_APPROVED_COMMAND,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The settled bar's one-line history: verdict counts folded from the effective
|
|
467
|
+
* decisions ("2 kept · 1 discarded"), with an honest "not reviewed" bucket for
|
|
468
|
+
* files a terminated set never decided. Falls back to a plain changed-count
|
|
469
|
+
* when nothing was decided at all.
|
|
470
|
+
*/
|
|
471
|
+
function settledSummary(
|
|
472
|
+
total: number,
|
|
473
|
+
changes: readonly CapturedFileChange[],
|
|
474
|
+
effectiveVerdicts: ReadonlyMap<string, FileDecisionAction>,
|
|
475
|
+
): string {
|
|
476
|
+
let kept = 0;
|
|
477
|
+
let discarded = 0;
|
|
478
|
+
for (const c of changes) {
|
|
479
|
+
const v = effectiveVerdicts.get(c.id);
|
|
480
|
+
if (v === FileDecisionAction.APPROVE) kept++;
|
|
481
|
+
else if (v === FileDecisionAction.REJECT) discarded++;
|
|
482
|
+
}
|
|
483
|
+
const undecided = total - kept - discarded;
|
|
484
|
+
if (kept === 0 && discarded === 0) {
|
|
485
|
+
return `${total} file${total === 1 ? "" : "s"} changed`;
|
|
486
|
+
}
|
|
487
|
+
const parts: string[] = [];
|
|
488
|
+
if (kept > 0) parts.push(`${kept} kept`);
|
|
489
|
+
if (discarded > 0) parts.push(`${discarded} discarded`);
|
|
490
|
+
if (undecided > 0) parts.push(`${undecided} not reviewed`);
|
|
491
|
+
return parts.join(" · ");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The bar expander's disclosure chevron (rotates when open). */
|
|
495
|
+
function ExpanderChevron({ expanded }: { expanded: boolean }) {
|
|
496
|
+
return (
|
|
497
|
+
<svg
|
|
498
|
+
width="10"
|
|
499
|
+
height="10"
|
|
500
|
+
viewBox="0 0 10 10"
|
|
501
|
+
fill="none"
|
|
502
|
+
stroke="currentColor"
|
|
503
|
+
strokeWidth="1.5"
|
|
504
|
+
strokeLinecap="round"
|
|
505
|
+
strokeLinejoin="round"
|
|
506
|
+
className={cn(
|
|
507
|
+
"shrink-0 transition-transform duration-150",
|
|
508
|
+
expanded && "rotate-90",
|
|
509
|
+
)}
|
|
510
|
+
aria-hidden="true"
|
|
511
|
+
>
|
|
512
|
+
<path d="M3.5 2L6.5 5L3.5 8" />
|
|
513
|
+
</svg>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// FileChangeReviewRow — one file paired with its Keep/Discard verdict
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
interface FileChangeReviewRowProps {
|
|
522
|
+
readonly change: CapturedFileChange;
|
|
523
|
+
/** The file's committed verdict (server truth), or null if undecided. */
|
|
524
|
+
readonly verdict: FileDecisionAction | null;
|
|
525
|
+
/** True while THIS file's decision (or a whole-set decision) is in flight. */
|
|
526
|
+
readonly isSubmitting: boolean;
|
|
527
|
+
/** This file's last failed decision, or null — surfaced beside its control. */
|
|
528
|
+
readonly error: Error | null;
|
|
529
|
+
readonly onDecide: (change: CapturedFileChange, action: FileDecisionAction) => void;
|
|
530
|
+
/** Diff-rich body vs compact list line — see {@link FileReviewCardProps.showDiffs}. */
|
|
531
|
+
readonly showDiffs: boolean;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* One reviewable file with its Keep/Discard control. The decision machinery
|
|
536
|
+
* (verdict radiogroup, block-reason note, in-card error) is identical in both
|
|
537
|
+
* body modes; only the file presentation varies — the bounded diff, or the
|
|
538
|
+
* compact {@link FileListRow} line with the control trailing it.
|
|
539
|
+
*/
|
|
540
|
+
const FileChangeReviewRow = memo(function FileChangeReviewRow({
|
|
541
|
+
change,
|
|
542
|
+
verdict,
|
|
543
|
+
isSubmitting,
|
|
544
|
+
error,
|
|
545
|
+
onDecide,
|
|
546
|
+
showDiffs,
|
|
547
|
+
}: FileChangeReviewRowProps) {
|
|
548
|
+
// One classification per change drives BOTH the disabled Keep and its reason,
|
|
549
|
+
// so the two can never disagree (single source of truth).
|
|
550
|
+
const reviewability = fileReviewability(change);
|
|
551
|
+
const blocked = reviewability.kind !== "reviewable";
|
|
552
|
+
const reasonId = useId();
|
|
553
|
+
const verdictControl = (
|
|
554
|
+
<FileVerdictControl
|
|
555
|
+
change={change}
|
|
556
|
+
reviewability={reviewability}
|
|
557
|
+
verdict={verdict}
|
|
558
|
+
isSubmitting={isSubmitting}
|
|
559
|
+
onDecide={onDecide}
|
|
560
|
+
describedById={blocked ? reasonId : undefined}
|
|
561
|
+
/>
|
|
562
|
+
);
|
|
563
|
+
return (
|
|
564
|
+
<div className="space-y-1.5">
|
|
565
|
+
{showDiffs ? (
|
|
566
|
+
<>
|
|
567
|
+
<CaptureBadge change={change} />
|
|
568
|
+
<CapturedChangeDiff change={change} />
|
|
569
|
+
{verdictControl}
|
|
570
|
+
</>
|
|
571
|
+
) : (
|
|
572
|
+
<FileListRow change={change} trailing={verdictControl} />
|
|
573
|
+
)}
|
|
574
|
+
{blocked && <BlockReasonNote reviewability={reviewability} id={reasonId} />}
|
|
575
|
+
{error && (
|
|
576
|
+
<InCardDecisionError
|
|
577
|
+
error={error}
|
|
578
|
+
leadIn="save"
|
|
579
|
+
cursorTarget="file-review-file-error"
|
|
580
|
+
/>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
// SettledFileRow — one file paired with its committed verdict (read-only)
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
interface SettledFileRowProps {
|
|
591
|
+
readonly change: CapturedFileChange;
|
|
592
|
+
/** The file's committed verdict, or null if the set was never decided. */
|
|
593
|
+
readonly verdict: FileDecisionAction | null;
|
|
594
|
+
/** Diff-rich body vs compact list line — see {@link FileReviewCardProps.showDiffs}. */
|
|
595
|
+
readonly showDiffs: boolean;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* A read-only file row for a settled change set: the capture provenance badge
|
|
600
|
+
* and a static verdict badge, with the before/after diff in diff mode or the
|
|
601
|
+
* compact list line otherwise. No decision controls — the verdict is history,
|
|
602
|
+
* not an action. Used by the read-only {@link FileReviewCard}
|
|
603
|
+
* (`interactive={false}`) for decided/reconciled sets and terminal executions.
|
|
604
|
+
*/
|
|
605
|
+
const SettledFileRow = memo(function SettledFileRow({
|
|
606
|
+
change,
|
|
607
|
+
verdict,
|
|
608
|
+
showDiffs,
|
|
609
|
+
}: SettledFileRowProps) {
|
|
610
|
+
if (!showDiffs) {
|
|
611
|
+
return <FileListRow change={change} trailing={<VerdictBadge verdict={verdict} />} />;
|
|
612
|
+
}
|
|
613
|
+
return (
|
|
614
|
+
<div className="space-y-1.5">
|
|
615
|
+
<div className="flex items-center gap-2">
|
|
616
|
+
<CaptureBadge change={change} />
|
|
617
|
+
<VerdictBadge verdict={verdict} />
|
|
618
|
+
</div>
|
|
619
|
+
<CapturedChangeDiff change={change} />
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// CapturedChangeDiff — a captured change's bounded diff (showDiffs mode)
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Projects a {@link CapturedFileChange} onto the display `FileChange` and
|
|
630
|
+
* renders its bounded diff — the one place the diff body renders, shared by the
|
|
631
|
+
* interactive, settled, and single-file branches of the diff-mode body.
|
|
632
|
+
*/
|
|
633
|
+
function CapturedChangeDiff({ change }: { change: CapturedFileChange }) {
|
|
634
|
+
const adapted = useMemo(() => toDisplayFileChange(change), [change]);
|
|
635
|
+
return <FileChangeDiff change={adapted} bounded />;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// FileListRow — one file as a compact list line (list mode)
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* One file of the change set as a compact, deliberately inert list line: kind
|
|
644
|
+
* letter, path, provenance badge, and an optional trailing slot (the verdict
|
|
645
|
+
* control while reviewing; the committed verdict once settled).
|
|
646
|
+
*
|
|
647
|
+
* The path is plain text — not `FilePathLink` — because the list is an
|
|
648
|
+
* inventory of what the decision covers, not a navigation surface: the diffs
|
|
649
|
+
* live on the transcript's stamped edit rows. The full path stays available on
|
|
650
|
+
* hover via `title`. A rename shows its source path so the move reads in one
|
|
651
|
+
* line.
|
|
652
|
+
*/
|
|
653
|
+
function FileListRow({
|
|
654
|
+
change,
|
|
655
|
+
trailing,
|
|
656
|
+
}: {
|
|
657
|
+
readonly change: CapturedFileChange;
|
|
658
|
+
readonly trailing?: React.ReactNode;
|
|
659
|
+
}) {
|
|
660
|
+
const path = change.pathAfter || change.pathBefore;
|
|
661
|
+
const { dir, base } = splitDisplayPath(path);
|
|
662
|
+
const renamedFrom =
|
|
663
|
+
change.kind === FileChangeKind.RENAME && change.pathBefore !== change.pathAfter
|
|
664
|
+
? change.pathBefore
|
|
665
|
+
: "";
|
|
666
|
+
return (
|
|
667
|
+
<div
|
|
668
|
+
className="flex flex-wrap items-center gap-x-2 gap-y-1"
|
|
669
|
+
data-cursor-target="file-review-list-row"
|
|
670
|
+
>
|
|
671
|
+
<FileKindBadge kind={change.kind} />
|
|
672
|
+
{/* The directory truncates (context); the base never shrinks (identity) —
|
|
673
|
+
the same filename-first rule FilePathLink established. */}
|
|
674
|
+
<span
|
|
675
|
+
className="flex min-w-0 flex-1 items-center font-mono text-xs text-foreground"
|
|
676
|
+
title={path}
|
|
677
|
+
>
|
|
678
|
+
{renamedFrom && (
|
|
679
|
+
<span className="min-w-0 truncate text-muted-foreground-faint">
|
|
680
|
+
{renamedFrom} →
|
|
681
|
+
</span>
|
|
682
|
+
)}
|
|
683
|
+
{dir && (
|
|
684
|
+
<span className="min-w-0 truncate text-muted-foreground-faint">{dir}</span>
|
|
685
|
+
)}
|
|
686
|
+
<span className="shrink-0">{base}</span>
|
|
687
|
+
</span>
|
|
688
|
+
<CaptureBadge change={change} />
|
|
689
|
+
{trailing}
|
|
690
|
+
</div>
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** The kind letter, tone, and a11y name for each wire {@link FileChangeKind}. */
|
|
695
|
+
const KIND_BADGE: Partial<
|
|
696
|
+
Record<FileChangeKind, { letter: string; colorClass: string; label: string }>
|
|
697
|
+
> = {
|
|
698
|
+
[FileChangeKind.ADD]: {
|
|
699
|
+
letter: "A",
|
|
700
|
+
colorClass: "text-diff-added-fg",
|
|
701
|
+
label: "added",
|
|
702
|
+
},
|
|
703
|
+
[FileChangeKind.MODIFY]: {
|
|
704
|
+
letter: "M",
|
|
705
|
+
colorClass: "text-diff-hunk-header-fg",
|
|
706
|
+
label: "modified",
|
|
707
|
+
},
|
|
708
|
+
[FileChangeKind.DELETE]: {
|
|
709
|
+
letter: "D",
|
|
710
|
+
colorClass: "text-diff-removed-fg",
|
|
711
|
+
label: "deleted",
|
|
712
|
+
},
|
|
713
|
+
[FileChangeKind.RENAME]: {
|
|
714
|
+
letter: "R",
|
|
715
|
+
colorClass: "text-diff-hunk-header-fg",
|
|
716
|
+
label: "renamed",
|
|
717
|
+
},
|
|
718
|
+
// A binary change is a modification whose diff is not text; the letter stays
|
|
719
|
+
// M and the binary-ness is told by the row's "Keep anyway" + reason note.
|
|
720
|
+
[FileChangeKind.BINARY_CHANGE]: {
|
|
721
|
+
letter: "M",
|
|
722
|
+
colorClass: "text-diff-hunk-header-fg",
|
|
723
|
+
label: "modified (binary)",
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* The single-letter change-kind marker — the same M/A/D visual vocabulary as
|
|
729
|
+
* the version-history `DiffFileList`, extended with R for captured renames.
|
|
730
|
+
* The letter itself is the non-color channel; the full word is exposed to
|
|
731
|
+
* assistive tech. An UNSPECIFIED kind renders nothing rather than guessing.
|
|
732
|
+
*/
|
|
733
|
+
function FileKindBadge({ kind }: { kind: FileChangeKind }) {
|
|
734
|
+
const badge = KIND_BADGE[kind];
|
|
735
|
+
if (!badge) return null;
|
|
736
|
+
return (
|
|
737
|
+
<span
|
|
738
|
+
className={cn("shrink-0 font-mono text-[10px] font-bold", badge.colorClass)}
|
|
739
|
+
aria-label={badge.label}
|
|
740
|
+
>
|
|
741
|
+
{badge.letter}
|
|
742
|
+
</span>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* A change's committed verdict as a static badge: APPROVE reads "Kept" (the
|
|
748
|
+
* bytes were reconciled into the workspace), REJECT reads "Discarded" (reverted
|
|
749
|
+
* to baseline), and an undecided change (a set terminated mid-review) reads "Not
|
|
750
|
+
* reviewed". Uses the same diff add/remove tokens the card uses elsewhere, so it
|
|
751
|
+
* introduces no new token.
|
|
752
|
+
*/
|
|
753
|
+
function VerdictBadge({ verdict }: { verdict: FileDecisionAction | null }) {
|
|
754
|
+
if (verdict === FileDecisionAction.APPROVE) {
|
|
755
|
+
return (
|
|
756
|
+
<span className="shrink-0 text-[11px] font-medium text-diff-added-fg">Kept</span>
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (verdict === FileDecisionAction.REJECT) {
|
|
760
|
+
return (
|
|
761
|
+
<span className="shrink-0 text-[11px] font-medium text-diff-removed-fg">
|
|
762
|
+
Discarded
|
|
763
|
+
</span>
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
return (
|
|
767
|
+
<span className="shrink-0 text-[11px] italic text-muted-foreground">
|
|
768
|
+
Not reviewed
|
|
769
|
+
</span>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
// FileVerdictControl — a Keep|Discard segmented control for one file
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
interface FileVerdictControlProps {
|
|
778
|
+
readonly change: CapturedFileChange;
|
|
779
|
+
/** The file's reviewability — `Keep` is available only when `reviewable`. */
|
|
780
|
+
readonly reviewability: FileReviewability;
|
|
781
|
+
readonly verdict: FileDecisionAction | null;
|
|
782
|
+
readonly isSubmitting: boolean;
|
|
783
|
+
readonly onDecide: (change: CapturedFileChange, action: FileDecisionAction) => void;
|
|
784
|
+
/** Id of the reason note describing why `Keep` is unavailable (a11y). */
|
|
785
|
+
readonly describedById?: string;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* The per-file decision affordance: a two-option segmented control modeled as a
|
|
790
|
+
* `radiogroup` (the established idiom across the SDK — Keep XOR Discard is a
|
|
791
|
+
* persistent, mutually-exclusive choice because the card stays visible after
|
|
792
|
+
* each file decision). The committed verdict reads as `aria-checked`; clicking
|
|
793
|
+
* the other option flips it (a new last-write-wins decision).
|
|
794
|
+
*
|
|
795
|
+
* `Keep` behavior by reviewability: a `reviewable` file keeps normally; a
|
|
796
|
+
* `binary` file has no text diff but reconcilable bytes, so `Keep` is enabled as
|
|
797
|
+
* an explicit "Keep anyway" that carries the acknowledgment (DD-16); an
|
|
798
|
+
* `unavailable` file (secret-withheld / size-elided) has no keepable bytes, so
|
|
799
|
+
* `Keep` stays disabled — it can only be discarded. `describedById` associates
|
|
800
|
+
* the group with the note explaining the binary/unavailable case.
|
|
801
|
+
*/
|
|
802
|
+
function FileVerdictControl({
|
|
803
|
+
change,
|
|
804
|
+
reviewability,
|
|
805
|
+
verdict,
|
|
806
|
+
isSubmitting,
|
|
807
|
+
onDecide,
|
|
808
|
+
describedById,
|
|
809
|
+
}: FileVerdictControlProps) {
|
|
810
|
+
// Optimistic: show the clicked option immediately; the server's decisions
|
|
811
|
+
// projection then confirms it (or, on failure, it reverts to `verdict`).
|
|
812
|
+
const [pending, setPending] = useState<FileDecisionAction | null>(null);
|
|
813
|
+
useEffect(() => {
|
|
814
|
+
if (!isSubmitting) setPending(null);
|
|
815
|
+
}, [isSubmitting]);
|
|
816
|
+
|
|
817
|
+
const effective = pending ?? verdict;
|
|
818
|
+
const path = change.pathAfter || change.pathBefore;
|
|
819
|
+
// A binary file is keepable via an explicit acknowledgment; only a truly
|
|
820
|
+
// unavailable diff (no captured bytes) leaves Keep disabled.
|
|
821
|
+
const isBinary = reviewability.kind === "binary";
|
|
822
|
+
const keepDisabled = reviewability.kind === "unavailable";
|
|
823
|
+
const keepLabel = isBinary ? "Keep anyway" : "Keep";
|
|
824
|
+
const keepAccessibleLabel = isBinary
|
|
825
|
+
? `Keep ${path} anyway (binary — no text diff)`
|
|
826
|
+
: `Keep ${path}`;
|
|
827
|
+
|
|
828
|
+
const decide = useCallback(
|
|
829
|
+
(action: FileDecisionAction) => {
|
|
830
|
+
if (isSubmitting) return;
|
|
831
|
+
setPending(action);
|
|
832
|
+
onDecide(change, action);
|
|
833
|
+
},
|
|
834
|
+
[isSubmitting, onDecide, change],
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
return (
|
|
838
|
+
<div
|
|
839
|
+
role="radiogroup"
|
|
840
|
+
aria-label={`Decision for ${path}`}
|
|
841
|
+
aria-describedby={describedById}
|
|
842
|
+
className="flex items-center gap-1.5"
|
|
843
|
+
>
|
|
844
|
+
<VerdictOption
|
|
845
|
+
label={keepLabel}
|
|
846
|
+
accessibleLabel={keepAccessibleLabel}
|
|
847
|
+
tone="keep"
|
|
848
|
+
selected={effective === FileDecisionAction.APPROVE}
|
|
849
|
+
busy={isSubmitting && pending === FileDecisionAction.APPROVE}
|
|
850
|
+
disabled={isSubmitting || keepDisabled}
|
|
851
|
+
onClick={() => decide(FileDecisionAction.APPROVE)}
|
|
852
|
+
/>
|
|
853
|
+
<VerdictOption
|
|
854
|
+
label="Discard"
|
|
855
|
+
accessibleLabel={`Discard ${path}`}
|
|
856
|
+
tone="discard"
|
|
857
|
+
selected={effective === FileDecisionAction.REJECT}
|
|
858
|
+
busy={isSubmitting && pending === FileDecisionAction.REJECT}
|
|
859
|
+
disabled={isSubmitting}
|
|
860
|
+
onClick={() => decide(FileDecisionAction.REJECT)}
|
|
861
|
+
/>
|
|
862
|
+
</div>
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
interface VerdictOptionProps {
|
|
867
|
+
readonly label: string;
|
|
868
|
+
readonly accessibleLabel: string;
|
|
869
|
+
readonly tone: "keep" | "discard";
|
|
870
|
+
readonly selected: boolean;
|
|
871
|
+
readonly busy: boolean;
|
|
872
|
+
readonly disabled: boolean;
|
|
873
|
+
readonly onClick: () => void;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Quiet at rest (ghost), tinted when selected — the verdict is signalled by both
|
|
877
|
+
// fill AND tone (not color alone), and every property flows through `--stgm-*`
|
|
878
|
+
// tokens with no opacity modifiers.
|
|
879
|
+
const VERDICT_BASE = cn(
|
|
880
|
+
"inline-flex items-center justify-center gap-1.5 rounded-md border px-2.5 py-1",
|
|
881
|
+
"text-xs font-medium transition-colors",
|
|
882
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
883
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const VERDICT_REST = "border-border text-muted-foreground hover:bg-accent-hover hover:text-foreground";
|
|
887
|
+
|
|
888
|
+
const VERDICT_SELECTED: Record<VerdictOptionProps["tone"], string> = {
|
|
889
|
+
keep: "border-success bg-success-subtle text-success",
|
|
890
|
+
discard: "border-destructive bg-destructive-subtle text-destructive",
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
function VerdictOption({
|
|
894
|
+
label,
|
|
895
|
+
accessibleLabel,
|
|
896
|
+
tone,
|
|
897
|
+
selected,
|
|
898
|
+
busy,
|
|
899
|
+
disabled,
|
|
900
|
+
onClick,
|
|
901
|
+
}: VerdictOptionProps) {
|
|
902
|
+
return (
|
|
903
|
+
<button
|
|
904
|
+
type="button"
|
|
905
|
+
role="radio"
|
|
906
|
+
aria-checked={selected}
|
|
907
|
+
aria-label={accessibleLabel}
|
|
908
|
+
disabled={disabled}
|
|
909
|
+
onClick={onClick}
|
|
910
|
+
data-cursor-target={`file-review-${tone}`}
|
|
911
|
+
className={cn(VERDICT_BASE, selected ? VERDICT_SELECTED[tone] : VERDICT_REST)}
|
|
912
|
+
>
|
|
913
|
+
{busy ? <SpinnerIcon /> : null}
|
|
914
|
+
{label}
|
|
915
|
+
</button>
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// ---------------------------------------------------------------------------
|
|
920
|
+
// CaptureBadge — provenance for a file captured outside normal git tracking
|
|
921
|
+
// ---------------------------------------------------------------------------
|
|
922
|
+
|
|
923
|
+
/** Non-default provenance the reviewer should be told about, with its a11y text. */
|
|
924
|
+
const PROVENANCE: Partial<Record<FileCaptureClass, { label: string; aria: string }>> = {
|
|
925
|
+
[FileCaptureClass.GIT_IGNORED_CAPTURED]: {
|
|
926
|
+
label: "gitignored",
|
|
927
|
+
aria: "This file is ignored by git; the change was captured for review",
|
|
928
|
+
},
|
|
929
|
+
[FileCaptureClass.NON_GIT_CAS]: {
|
|
930
|
+
label: "outside git",
|
|
931
|
+
aria: "This file is outside the git repository; the change was captured for review",
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* A small provenance pill for a file captured outside normal git tracking —
|
|
937
|
+
* gitignored or non-git (CAS). Renders nothing for ordinary git-tracked (or
|
|
938
|
+
* untracked-but-new) files, so the badge appears only when it carries real
|
|
939
|
+
* signal: this change is not part of the repo's tracked history.
|
|
940
|
+
*/
|
|
941
|
+
function CaptureBadge({ change }: { change: CapturedFileChange }) {
|
|
942
|
+
const provenance = PROVENANCE[change.captureClass];
|
|
943
|
+
if (!provenance) return null;
|
|
944
|
+
return (
|
|
945
|
+
<span
|
|
946
|
+
className="inline-flex w-fit items-center rounded border border-border bg-muted-subtle px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground"
|
|
947
|
+
aria-label={provenance.aria}
|
|
948
|
+
data-cursor-target="file-review-capture"
|
|
949
|
+
>
|
|
950
|
+
{provenance.label}
|
|
951
|
+
</span>
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ---------------------------------------------------------------------------
|
|
956
|
+
// BlockReasonNote — why a file's Keep is unavailable (associated via aria)
|
|
957
|
+
// ---------------------------------------------------------------------------
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* The in-place explanation for a non-reviewable file, associated with the verdict
|
|
961
|
+
* control via `aria-describedby`. Honest per {@link fileReviewability}: a `binary`
|
|
962
|
+
* change has no text diff but is keepable-as-bytes (an explicit "Keep anyway"); an
|
|
963
|
+
* `unavailable` change's diff cannot be shown at all and is discard-only, with the
|
|
964
|
+
* specific cause the runner recorded (secret-withheld vs size-dropped, doc 15). A
|
|
965
|
+
* reviewable file renders no note.
|
|
966
|
+
*/
|
|
967
|
+
function BlockReasonNote({
|
|
968
|
+
reviewability,
|
|
969
|
+
id,
|
|
970
|
+
}: {
|
|
971
|
+
reviewability: FileReviewability;
|
|
972
|
+
id: string;
|
|
973
|
+
}) {
|
|
974
|
+
const text = blockReasonText(reviewability);
|
|
975
|
+
if (!text) return null;
|
|
976
|
+
return (
|
|
977
|
+
<p
|
|
978
|
+
id={id}
|
|
979
|
+
className="text-[11px] italic text-muted-foreground"
|
|
980
|
+
data-cursor-target="file-review-block-reason"
|
|
981
|
+
>
|
|
982
|
+
{text}
|
|
983
|
+
</p>
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** The per-file reason copy for a non-reviewable change (null when reviewable). */
|
|
988
|
+
function blockReasonText(reviewability: FileReviewability): string | null {
|
|
989
|
+
switch (reviewability.kind) {
|
|
990
|
+
case "binary":
|
|
991
|
+
return "Binary file — no text diff to review. Keep it as-is (its exact bytes are applied) or discard it.";
|
|
992
|
+
case "unavailable":
|
|
993
|
+
switch (reviewability.reason) {
|
|
994
|
+
case "secret":
|
|
995
|
+
return "Contents withheld — this path looks like a secret, so it was never captured for review and can only be discarded.";
|
|
996
|
+
case "size":
|
|
997
|
+
return "Diff too large to display — the full contents were elided to stay within limits, so this change can only be discarded.";
|
|
998
|
+
default:
|
|
999
|
+
return "The full diff isn't available to review, so this change can only be discarded.";
|
|
1000
|
+
}
|
|
1001
|
+
default:
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* The whole-set incompleteness copy, accurate to which non-reviewable classes are
|
|
1008
|
+
* present (never the generic "binary or truncated"). Unavailable is reported
|
|
1009
|
+
* ahead of binary because it is the stronger "nothing to see" signal.
|
|
1010
|
+
*
|
|
1011
|
+
* A `binaryOnly` set is the keepable case (DD-17): binary files are the only
|
|
1012
|
+
* blocker, so the copy explains that "Keep all" is available rather than saying
|
|
1013
|
+
* the set can't be approved. A `blocked` set (something unavailable) keeps the
|
|
1014
|
+
* discard-oriented copy.
|
|
1015
|
+
*/
|
|
1016
|
+
function incompleteNotice(
|
|
1017
|
+
showPerFile: boolean,
|
|
1018
|
+
summary: { hasBinary: boolean; hasUnavailable: boolean },
|
|
1019
|
+
binaryOnly: boolean,
|
|
1020
|
+
): string {
|
|
1021
|
+
if (binaryOnly) {
|
|
1022
|
+
// Every non-binary file is reviewable; only binaries have no text diff.
|
|
1023
|
+
return showPerFile
|
|
1024
|
+
? "This set includes binary files with no text diff. Keep all keeps every file as-is (their exact bytes are applied), or decide each file below."
|
|
1025
|
+
: "This is a binary change with no text diff. Keep it as-is (its exact bytes are applied) or discard it.";
|
|
1026
|
+
}
|
|
1027
|
+
if (showPerFile) {
|
|
1028
|
+
if (summary.hasUnavailable) {
|
|
1029
|
+
return "Part of this change isn't available to review, so the whole set can't be approved — keep the reviewable files and discard the rest.";
|
|
1030
|
+
}
|
|
1031
|
+
if (summary.hasBinary) {
|
|
1032
|
+
return "This set includes a binary change with no text diff, so it can't be approved at once — decide each file below (keep the reviewable ones; keep or discard the binary).";
|
|
1033
|
+
}
|
|
1034
|
+
return "Some files can't be reviewed completely, so the whole set can't be approved at once — keep the reviewable files and discard the rest.";
|
|
1035
|
+
}
|
|
1036
|
+
if (summary.hasUnavailable) {
|
|
1037
|
+
return "This change isn't available to review, so it can't be approved — it can only be discarded.";
|
|
1038
|
+
}
|
|
1039
|
+
if (summary.hasBinary) {
|
|
1040
|
+
return "This is a binary change with no text diff, so it can't be approved as a complete change — it can only be discarded.";
|
|
1041
|
+
}
|
|
1042
|
+
return "This change can't be reviewed completely, so it can't be approved as a complete change.";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ---------------------------------------------------------------------------
|
|
1046
|
+
// ReviewProgress — how many files still need a verdict before the turn resumes
|
|
1047
|
+
// ---------------------------------------------------------------------------
|
|
1048
|
+
|
|
1049
|
+
function ReviewProgress({ decided, total }: { decided: number; total: number }) {
|
|
1050
|
+
return (
|
|
1051
|
+
<p role="status" className="text-[11px] text-muted-foreground">
|
|
1052
|
+
{decided} of {total} files reviewed
|
|
1053
|
+
{decided > 0 && decided < total && " — decide the rest to continue"}
|
|
1054
|
+
</p>
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1059
|
+
// Helpers
|
|
1060
|
+
// ---------------------------------------------------------------------------
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Fold the ledger-ordered decisions into each file's current verdict, last
|
|
1064
|
+
* write wins (the precedence the runner reconcile applies). Only FILE-scoped
|
|
1065
|
+
* decisions are per-file; a CHANGE_SET decision cannot coexist with an
|
|
1066
|
+
* AWAITING_REVIEW set, so it is ignored defensively.
|
|
1067
|
+
*/
|
|
1068
|
+
function deriveFileVerdicts(
|
|
1069
|
+
decisions: readonly FileDecision[],
|
|
1070
|
+
): Map<string, FileDecisionAction> {
|
|
1071
|
+
const byFileId = new Map<string, FileDecisionAction>();
|
|
1072
|
+
for (const d of decisions) {
|
|
1073
|
+
if (d.scope === FileDecisionScope.FILE && d.fileChangeId) {
|
|
1074
|
+
byFileId.set(d.fileChangeId, d.action);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return byFileId;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* The whole-set action labels, sharpened by file count and review progress. A
|
|
1082
|
+
* `binaryOnly` set (DD-17) reads as "Keep all" rather than "Approve all": the
|
|
1083
|
+
* bulk keep carries the binary acknowledgment, and "Keep" is the same verb the
|
|
1084
|
+
* per-file binary control uses ("Keep anyway").
|
|
1085
|
+
*/
|
|
1086
|
+
function bulkLabels(
|
|
1087
|
+
total: number,
|
|
1088
|
+
decided: number,
|
|
1089
|
+
binaryOnly: boolean,
|
|
1090
|
+
): { approve: string; reject: string } {
|
|
1091
|
+
const approveVerb = binaryOnly ? "Keep" : "Approve";
|
|
1092
|
+
if (total <= 1) return { approve: approveVerb, reject: "Reject" };
|
|
1093
|
+
if (decided > 0 && decided < total) {
|
|
1094
|
+
return { approve: `${approveVerb} remaining`, reject: "Reject remaining" };
|
|
1095
|
+
}
|
|
1096
|
+
return { approve: `${approveVerb} all`, reject: "Reject all" };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// ---------------------------------------------------------------------------
|
|
1100
|
+
// Inline SVG icon
|
|
1101
|
+
// ---------------------------------------------------------------------------
|
|
1102
|
+
|
|
1103
|
+
function SpinnerIcon() {
|
|
1104
|
+
return (
|
|
1105
|
+
<svg
|
|
1106
|
+
width="10"
|
|
1107
|
+
height="10"
|
|
1108
|
+
viewBox="0 0 12 12"
|
|
1109
|
+
fill="none"
|
|
1110
|
+
stroke="currentColor"
|
|
1111
|
+
strokeWidth="1.5"
|
|
1112
|
+
className="animate-spin"
|
|
1113
|
+
aria-hidden="true"
|
|
1114
|
+
>
|
|
1115
|
+
<path d="M6 1.5A4.5 4.5 0 1 1 1.5 6" strokeLinecap="round" />
|
|
1116
|
+
</svg>
|
|
1117
|
+
);
|
|
1118
|
+
}
|