@cat-factory/app 0.302.2 → 0.303.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/README.md +16 -0
- package/app/app.config.ts +19 -10
- package/app/assets/css/main.css +13 -13
- package/app/assets/css/prose.css +22 -22
- package/app/assets/css/tokens.css +298 -0
- package/app/components/assistant/AssistantModal.vue +22 -22
- package/app/components/auth/AuthGate.vue +1 -1
- package/app/components/auth/BackendMisconfiguredScreen.vue +10 -10
- package/app/components/auth/LoginScreen.vue +34 -36
- package/app/components/auth/ResetPasswordScreen.vue +12 -12
- package/app/components/auth/UserMenu.vue +4 -4
- package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +27 -25
- package/app/components/binaryOutput/BinaryOutputReport.vue +15 -15
- package/app/components/binaryOutput/StoredAssetView.vue +5 -5
- package/app/components/board/AddTaskModal.vue +17 -20
- package/app/components/board/AgentFailureCard.vue +11 -11
- package/app/components/board/AgentFailureHistory.vue +2 -2
- package/app/components/board/BoardCanvas.vue +6 -5
- package/app/components/board/CreateInitiativeModal.vue +8 -10
- package/app/components/board/DependencyConnectOverlay.vue +1 -1
- package/app/components/board/FailureHistoryList.vue +7 -7
- package/app/components/board/RecurringPipelineModal.vue +17 -17
- package/app/components/board/ReviewFrictionDialog.vue +6 -6
- package/app/components/board/StepExecutionHistory.vue +13 -13
- package/app/components/board/TaskDependencyEdges.vue +8 -8
- package/app/components/board/nodes/AgentChip.vue +10 -10
- package/app/components/board/nodes/BlockNode.vue +29 -29
- package/app/components/board/nodes/DecisionBadge.vue +2 -2
- package/app/components/board/nodes/EpicNode.vue +14 -10
- package/app/components/board/nodes/FrameSwimlanes.vue +7 -9
- package/app/components/board/nodes/InitiativeCard.vue +8 -8
- package/app/components/board/nodes/LaneGroup.vue +1 -1
- package/app/components/board/nodes/LaneTask.vue +2 -2
- package/app/components/board/nodes/ResizeGrips.vue +4 -4
- package/app/components/board/nodes/TaskCard.vue +18 -18
- package/app/components/board/nodes/TaskLane.vue +5 -5
- package/app/components/board/nodes/TaskPipelineMini.vue +18 -18
- package/app/components/bootstrap/AdoptionReviewModal.vue +29 -26
- package/app/components/bootstrap/BootstrapModal.vue +21 -23
- package/app/components/bootstrap/BootstrapRunSteps.vue +1 -1
- package/app/components/brainstorm/BrainstormWindow.vue +30 -33
- package/app/components/bugFishing/BugFishingWindow.vue +49 -49
- package/app/components/clarity/ClarityReviewWindow.vue +30 -30
- package/app/components/common/AsyncViewError.vue +5 -5
- package/app/components/common/ClarificationItem.vue +8 -13
- package/app/components/common/ConfirmDialog.vue +4 -2
- package/app/components/common/DescriptorFields.vue +1 -1
- package/app/components/common/EmptyState.vue +3 -3
- package/app/components/common/InterviewGateNotice.vue +7 -7
- package/app/components/common/KeyboardShortcutsHelp.vue +1 -1
- package/app/components/common/MarkdownProse.vue +14 -13
- package/app/components/consensus/ConsensusSessionWindow.vue +26 -26
- package/app/components/context/ContextAttachmentFields.vue +17 -17
- package/app/components/docs/DocInterviewWindow.vue +14 -14
- package/app/components/documents/ContextDocumentPicker.vue +17 -17
- package/app/components/documents/DocumentImportModal.vue +7 -7
- package/app/components/documents/DocumentSourceConnectModal.vue +3 -3
- package/app/components/documents/DocumentSyncState.vue +5 -5
- package/app/components/documents/DocumentTemplatesModal.vue +15 -15
- package/app/components/documents/RepoContextDocPicker.vue +5 -5
- package/app/components/documents/SpawnPreviewModal.vue +12 -12
- package/app/components/documents/StartFromDesignModal.vue +11 -11
- package/app/components/documents/TaskContextDocs.vue +5 -5
- package/app/components/environments/EnvSetupStepper.vue +6 -6
- package/app/components/environments/EnvironmentStatusPanel.vue +17 -17
- package/app/components/environments/steps/EnvPickStep.vue +3 -3
- package/app/components/environments/steps/EnvPreflightStep.vue +9 -9
- package/app/components/environments/steps/EnvReviewStep.vue +25 -22
- package/app/components/environments/steps/EnvSaveStep.vue +7 -5
- package/app/components/environments/steps/JourneyStepNav.vue +1 -1
- package/app/components/focus/BlockFocusView.vue +17 -19
- package/app/components/followUp/FollowUpWindow.vue +23 -23
- package/app/components/forkDecision/ForkDecisionWindow.vue +37 -39
- package/app/components/foundational/FoundationalContractSummary.vue +7 -7
- package/app/components/foundational/FoundationalServiceCatalogList.vue +11 -11
- package/app/components/foundational/FoundationalServiceManager.vue +1 -1
- package/app/components/foundational/FoundationalServiceRegistry.vue +13 -13
- package/app/components/foundational/FoundationalServiceSources.vue +13 -13
- package/app/components/foundational/FoundationalSuppressions.vue +7 -7
- package/app/components/foundational/ServiceCatalogConnection.vue +10 -10
- package/app/components/fragments/FragmentLibraryManager.vue +41 -44
- package/app/components/fragments/FragmentSelector.vue +13 -13
- package/app/components/fragments/GitHubDocUrlImport.vue +1 -1
- package/app/components/gates/GateFailingCheckList.vue +5 -5
- package/app/components/gates/GateResultView.vue +50 -47
- package/app/components/github/AddServiceFromRepoModal.vue +14 -14
- package/app/components/github/BranchProtectionPreflight.vue +13 -13
- package/app/components/github/GitHubConnect.vue +7 -7
- package/app/components/github/GitHubOnboarding.vue +7 -7
- package/app/components/github/GitHubPanel.vue +37 -40
- package/app/components/github/RepoTreeBrowser.vue +18 -18
- package/app/components/humanTest/HumanTestWindow.vue +30 -33
- package/app/components/initiative/InitiativePlanNotice.vue +5 -5
- package/app/components/initiative/InitiativePlanReview.vue +27 -29
- package/app/components/initiative/InitiativePlanningWindow.vue +10 -10
- package/app/components/initiative/InitiativeTrackerWindow.vue +71 -65
- package/app/components/inputGate/InputGateNotice.vue +2 -4
- package/app/components/judge/JudgeResultView.vue +30 -32
- package/app/components/kaizen/KaizenPanel.vue +40 -36
- package/app/components/kaizen/KaizenStepStatus.vue +21 -21
- package/app/components/layout/AccountAuditLog.vue +10 -10
- package/app/components/layout/AccountDeploymentSettings.vue +20 -20
- package/app/components/layout/AccountFailureKindRules.vue +13 -13
- package/app/components/layout/AccountFoundationalSettings.vue +1 -1
- package/app/components/layout/AccountFragmentSettings.vue +1 -1
- package/app/components/layout/AccountModelPolicySettings.vue +9 -9
- package/app/components/layout/AccountPlatformAlertSettings.vue +11 -11
- package/app/components/layout/AccountRiskPolicySettings.vue +2 -2
- package/app/components/layout/AccountRunCredentialSettings.vue +5 -5
- package/app/components/layout/AccountSkillSettings.vue +1 -1
- package/app/components/layout/AccountTeamSettings.vue +23 -15
- package/app/components/layout/AiProvidersBanner.vue +11 -8
- package/app/components/layout/AppearanceSwitcher.vue +108 -0
- package/app/components/layout/BoardSwitcher.vue +8 -11
- package/app/components/layout/BoardToolbar.vue +3 -3
- package/app/components/layout/CommandBar.vue +7 -9
- package/app/components/layout/ConnectionStatusBanner.vue +2 -2
- package/app/components/layout/DefaultTestEnvBanner.vue +5 -5
- package/app/components/layout/GitHubPatBanner.vue +5 -5
- package/app/components/layout/GitHubPatPermissionsBanner.vue +16 -10
- package/app/components/layout/InfraSetupBanner.vue +6 -6
- package/app/components/layout/IntegrationsHub.vue +15 -15
- package/app/components/layout/LanguageSwitcher.vue +5 -5
- package/app/components/layout/ModelProvidersHub.vue +13 -16
- package/app/components/layout/NotificationsInbox.vue +10 -10
- package/app/components/layout/PersonalSetupModal.vue +8 -8
- package/app/components/layout/ProviderConfigBanner.vue +4 -4
- package/app/components/layout/RolePrompt.vue +7 -7
- package/app/components/layout/SideBar.vue +6 -4
- package/app/components/layout/SpendWarningBanner.vue +17 -14
- package/app/components/layout/TranslationWarningBanner.vue +4 -4
- package/app/components/layout/UiModeSwitcher.vue +13 -13
- package/app/components/layout/UiRoleSwitcher.vue +8 -8
- package/app/components/layout/WorkspaceMembersSettings.vue +15 -11
- package/app/components/media/ArtifactLightbox.vue +13 -13
- package/app/components/media/ImageCompare.vue +31 -24
- package/app/components/merge/MergeEffortChips.vue +2 -2
- package/app/components/notifications/NotificationSettingsPanel.vue +14 -12
- package/app/components/observability/OutcomeFilterChips.vue +6 -6
- package/app/components/observability/RunFailureSummary.vue +28 -25
- package/app/components/observability/StepMetricsBar.vue +26 -28
- package/app/components/observability/StepModelActivity.vue +2 -2
- package/app/components/observability/ToolCallList.vue +24 -24
- package/app/components/outcome/OutcomeSummaryWindow.vue +47 -47
- package/app/components/palettes/AgentPalette.vue +8 -8
- package/app/components/palettes/AgentTierSelect.vue +1 -1
- package/app/components/palettes/PipelinePurposeSelect.vue +1 -1
- package/app/components/panels/AgentStepDetail.vue +52 -51
- package/app/components/panels/AttemptEntryHeader.vue +4 -6
- package/app/components/panels/DecisionModal.vue +7 -7
- package/app/components/panels/GenericStructuredResultView.vue +7 -7
- package/app/components/panels/InspectorPanel.vue +17 -17
- package/app/components/panels/MergerResultView.vue +19 -19
- package/app/components/panels/ObservabilityPanel.vue +115 -123
- package/app/components/panels/OperatorDashboardPanel.vue +89 -97
- package/app/components/panels/ReportsPanel.vue +69 -66
- package/app/components/panels/ReportsSpendBreakdown.vue +9 -9
- package/app/components/panels/ResultWindowShell.vue +38 -38
- package/app/components/panels/RunDetailLoadState.vue +2 -2
- package/app/components/panels/StepContainerStatus.vue +9 -9
- package/app/components/panels/StepDelegatedStatus.logic.spec.ts +135 -0
- package/app/components/panels/StepDelegatedStatus.logic.ts +160 -0
- package/app/components/panels/StepDelegatedStatus.vue +181 -0
- package/app/components/panels/StepEffortReport.vue +16 -15
- package/app/components/panels/StepFragmentAdherence.vue +14 -10
- package/app/components/panels/StepMetadataCard.vue +74 -45
- package/app/components/panels/StepReproductionReport.vue +25 -21
- package/app/components/panels/StepRunMeta.vue +20 -20
- package/app/components/panels/StepTestReport.logic.spec.ts +8 -3
- package/app/components/panels/StepTestReport.logic.ts +4 -4
- package/app/components/panels/StepTestReport.vue +28 -28
- package/app/components/panels/StepToolServers.vue +14 -11
- package/app/components/panels/StepValidationReport.vue +7 -7
- package/app/components/panels/inspector/ContainerSummary.vue +9 -9
- package/app/components/panels/inspector/DocReferenceRepos.vue +3 -3
- package/app/components/panels/inspector/EnvProbeReport.vue +24 -27
- package/app/components/panels/inspector/EpicChildren.vue +8 -12
- package/app/components/panels/inspector/FrontendBindingsResolved.vue +11 -11
- package/app/components/panels/inspector/FrontendConfig.vue +37 -41
- package/app/components/panels/inspector/InitiativeInspector.vue +3 -3
- package/app/components/panels/inspector/InspectorSection.vue +6 -6
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +16 -19
- package/app/components/panels/inspector/ServiceConnections.vue +2 -2
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -2
- package/app/components/panels/inspector/ServiceSelfTests.vue +21 -17
- package/app/components/panels/inspector/ServiceTestConfig.vue +51 -59
- package/app/components/panels/inspector/ServiceTestSecrets.vue +5 -5
- package/app/components/panels/inspector/ServiceTestingContext.vue +3 -3
- package/app/components/panels/inspector/ServiceValidationConfig.vue +2 -2
- package/app/components/panels/inspector/TaskAgentConfig.vue +3 -3
- package/app/components/panels/inspector/TaskAprioriBranches.vue +8 -5
- package/app/components/panels/inspector/TaskDependencies.vue +1 -1
- package/app/components/panels/inspector/TaskEstimateBadge.vue +11 -11
- package/app/components/panels/inspector/TaskExecution.vue +17 -17
- package/app/components/panels/inspector/TaskReviewTarget.vue +2 -2
- package/app/components/panels/inspector/TaskRunSettings.vue +35 -37
- package/app/components/panels/inspector/TaskStructure.vue +3 -3
- package/app/components/pipeline/AgentKindIcon.vue +1 -1
- package/app/components/pipeline/AgentPromptEditor.vue +17 -17
- package/app/components/pipeline/BinaryOutputStepPicker.vue +42 -56
- package/app/components/pipeline/EstimateThresholdFields.vue +4 -4
- package/app/components/pipeline/GateConfigFields.vue +13 -10
- package/app/components/pipeline/IterationCapPrompt.vue +4 -2
- package/app/components/pipeline/PipelineBuilder.vue +47 -55
- package/app/components/pipeline/PipelineHealthModal.vue +27 -29
- package/app/components/pipeline/PipelinePicker.vue +10 -10
- package/app/components/pipeline/PipelinePreview.vue +10 -10
- package/app/components/pipeline/PipelineProgress.vue +64 -67
- package/app/components/prReview/PrReviewPhaseBadge.vue +7 -7
- package/app/components/prReview/PrReviewWindow.vue +75 -77
- package/app/components/providers/AiPresetMismatchDialog.vue +5 -5
- package/app/components/providers/AiProviderOnboardingModal.vue +9 -9
- package/app/components/providers/ApiKeysSection.vue +13 -13
- package/app/components/providers/PersonalCredentialModal.vue +2 -2
- package/app/components/providers/PersonalSubscriptionSection.vue +10 -10
- package/app/components/providers/SignInRequiredNotice.vue +1 -1
- package/app/components/providers/VendorCredentialsModal.vue +10 -10
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +13 -13
- package/app/components/ralph/RalphLoopResultView.vue +28 -30
- package/app/components/recurring/RecurrenceEditor.vue +2 -2
- package/app/components/requirements/RequirementsReviewWindow.vue +51 -58
- package/app/components/riskPolicy/RiskPolicyPicker.vue +12 -15
- package/app/components/riskPolicy/RiskPolicyPreview.vue +14 -14
- package/app/components/sandbox/SandboxPanel.vue +58 -59
- package/app/components/settings/AccountSettingsPanel.vue +1 -1
- package/app/components/settings/ApiTokensPanel.vue +10 -10
- package/app/components/settings/BudgetSettings.vue +18 -22
- package/app/components/settings/CapabilityCredentialsPanel.vue +15 -15
- package/app/components/settings/CloudflareHandlerSection.vue +20 -20
- package/app/components/settings/ComposeEnvironmentSetupSection.vue +13 -13
- package/app/components/settings/ConnectionTestVerdict.vue +3 -3
- package/app/components/settings/ConnectionWarnings.vue +1 -1
- package/app/components/settings/ConsensusGroupsSection.vue +27 -35
- package/app/components/settings/CustomManifestTypeEditor.vue +10 -10
- package/app/components/settings/DefaultProvisionTypeSection.vue +6 -6
- package/app/components/settings/InfraHandlersConfigurator.vue +24 -22
- package/app/components/settings/InfrastructureBackendPicker.vue +6 -6
- package/app/components/settings/InfrastructureWindow.vue +5 -5
- package/app/components/settings/IssueTrackerPanel.vue +29 -23
- package/app/components/settings/KubernetesEngineForm.vue +11 -11
- package/app/components/settings/KubernetesEnvironmentForm.vue +4 -4
- package/app/components/settings/LocalContainerPoolSettings.vue +6 -11
- package/app/components/settings/LocalModelEndpointsPanel.vue +20 -18
- package/app/components/settings/McpAuthorizeScreen.vue +13 -15
- package/app/components/settings/McpOAuthCallbackScreen.vue +16 -10
- package/app/components/settings/MergeClassRulesEditor.vue +6 -9
- package/app/components/settings/MergeRolePolicyEditor.vue +16 -16
- package/app/components/settings/ModelConfigurationPanel.vue +23 -30
- package/app/components/settings/ModelPresetHealthModal.vue +15 -13
- package/app/components/settings/ObservabilityConnectionPanel.vue +4 -4
- package/app/components/settings/OpenRouterCatalogPanel.vue +23 -19
- package/app/components/settings/PackageRegistriesPanel.vue +8 -8
- package/app/components/settings/ProviderConnectionTab.vue +8 -11
- package/app/components/settings/ProviderManifestEditor.vue +9 -9
- package/app/components/settings/ProviderPreferenceEditor.vue +8 -8
- package/app/components/settings/RiskPolicyCreateForm.vue +10 -10
- package/app/components/settings/RiskPolicyEditorRow.vue +14 -14
- package/app/components/settings/RiskPolicyHealthModal.vue +15 -13
- package/app/components/settings/RiskPolicyInheritedRow.vue +1 -1
- package/app/components/settings/RiskPolicyPanel.vue +10 -15
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +7 -7
- package/app/components/settings/SharedStacksPanel.vue +9 -12
- package/app/components/settings/TaskSourceCard.vue +4 -4
- package/app/components/settings/TaskTypeSuppressionsPanel.vue +6 -6
- package/app/components/settings/ToolServerChecklist.vue +36 -29
- package/app/components/settings/UsageSettings.vue +15 -15
- package/app/components/settings/UserSecretsSection.vue +9 -12
- package/app/components/settings/WorkspaceMetadataSettings.vue +5 -5
- package/app/components/settings/WorkspaceSettingsPanel.vue +47 -47
- package/app/components/skills/ReviewSkillQueue.vue +10 -10
- package/app/components/skills/SkillLibraryManager.vue +23 -20
- package/app/components/slack/SlackPanel.vue +13 -17
- package/app/components/spec/ServiceSpecWindow.vue +45 -42
- package/app/components/tasks/BugHuntModal.vue +24 -27
- package/app/components/tasks/ContextIssuePicker.vue +11 -11
- package/app/components/tasks/TaskContextIssues.vue +3 -3
- package/app/components/tasks/TaskImportModal.vue +7 -7
- package/app/components/tasks/TaskSourceConnectModal.vue +10 -8
- package/app/components/testing/TestReportWindow.vue +99 -82
- package/app/components/theme/ThemeImportModal.vue +115 -0
- package/app/components/tutorial/TutorialCatalogue.vue +9 -9
- package/app/components/tutorial/TutorialNudge.vue +5 -5
- package/app/components/tutorial/TutorialOverlay.vue +12 -12
- package/app/components/tutorial/TutorialPrompt.vue +6 -9
- package/app/components/vcs/GitLabConnect.vue +4 -4
- package/app/components/vcs/VcsConnectSurfaces.vue +2 -2
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +36 -36
- package/app/composables/api/client.ts +6 -1
- package/app/composables/usePipelineErrorToast.ts +17 -0
- package/app/modular/agent-kinds.ts +24 -0
- package/app/pages/index.vue +5 -5
- package/app/plugins/theme.client.ts +125 -0
- package/app/spa-loading-template.html +52 -14
- package/app/stores/agents.ts +1 -1
- package/app/stores/theme.spec.ts +56 -0
- package/app/stores/theme.ts +87 -0
- package/app/types/domain.ts +19 -0
- package/app/types/execution.ts +4 -0
- package/app/utils/agentOutput.ts +1 -1
- package/app/utils/binaryOutput.ts +6 -6
- package/app/utils/bootstrapSteps.ts +14 -14
- package/app/utils/catalog.ts +87 -83
- package/app/utils/colorTint.ts +10 -0
- package/app/utils/failureKinds.ts +1 -0
- package/app/utils/observability.ts +3 -3
- package/app/utils/pipelineRender.ts +13 -17
- package/app/utils/swimlanes.ts +4 -4
- package/app/utils/theme/bootPalette.spec.ts +54 -0
- package/app/utils/theme/bootPalette.ts +108 -0
- package/app/utils/theme/builtins.spec.ts +38 -0
- package/app/utils/theme/builtins.ts +55 -0
- package/app/utils/theme/css.ts +162 -0
- package/app/utils/theme/doc.ts +353 -0
- package/app/utils/theme/link.ts +93 -0
- package/app/utils/theme/presets.ts +509 -0
- package/app/utils/theme/theme.spec.ts +224 -0
- package/i18n/locales/de.json +61 -4
- package/i18n/locales/en.json +70 -4
- package/i18n/locales/es.json +61 -4
- package/i18n/locales/fr.json +61 -4
- package/i18n/locales/he.json +61 -4
- package/i18n/locales/it.json +61 -4
- package/i18n/locales/ja.json +61 -4
- package/i18n/locales/pl.json +61 -4
- package/i18n/locales/tr.json +61 -4
- package/i18n/locales/uk.json +61 -4
- package/nuxt.config.ts +37 -11
- package/package.json +9 -8
|
@@ -97,7 +97,7 @@ const props = withDefaults(
|
|
|
97
97
|
}>(),
|
|
98
98
|
{
|
|
99
99
|
icon: 'i-lucide-square',
|
|
100
|
-
iconClass: 'bg-
|
|
100
|
+
iconClass: 'bg-app-500/15 text-toned',
|
|
101
101
|
subtitle: undefined,
|
|
102
102
|
width: '3xl',
|
|
103
103
|
variant: 'stretch',
|
|
@@ -244,20 +244,20 @@ const binaryOutputSummary = computed(() => {
|
|
|
244
244
|
const effortOpen = ref(false)
|
|
245
245
|
const hint = computed(() => (effortReport.value ? effortHint(effortReport.value) : null))
|
|
246
246
|
const CHIP_CLASS = {
|
|
247
|
-
easy: 'bg-
|
|
248
|
-
moderate: 'bg-
|
|
249
|
-
hard: 'bg-
|
|
247
|
+
easy: 'bg-app-success-500/15 text-app-success-300',
|
|
248
|
+
moderate: 'bg-app-warning-500/15 text-app-warning-300',
|
|
249
|
+
hard: 'bg-app-error-500/15 text-app-error-300',
|
|
250
250
|
} as const
|
|
251
251
|
const chipClass = computed(() =>
|
|
252
252
|
effortReport.value ? CHIP_CLASS[effortBand(effortReport.value.difficulty)] : '',
|
|
253
253
|
)
|
|
254
254
|
|
|
255
255
|
const backdropClass = computed(() => [
|
|
256
|
-
'fixed inset-0 z-50 flex max-h-[100dvh] justify-center bg-
|
|
256
|
+
'fixed inset-0 z-50 flex max-h-[100dvh] justify-center bg-app-950/70 backdrop-blur-sm',
|
|
257
257
|
props.variant === 'centered' ? 'items-center p-4' : 'items-stretch',
|
|
258
258
|
])
|
|
259
259
|
const panelClass = computed(() => [
|
|
260
|
-
'flex w-full flex-col overflow-hidden rounded-2xl border border-
|
|
260
|
+
'flex w-full flex-col overflow-hidden rounded-2xl border border-default bg-default shadow-2xl',
|
|
261
261
|
RESULT_WINDOW_WIDTH_CLASS[props.width],
|
|
262
262
|
props.variant === 'centered' ? 'max-h-[90dvh]' : 'm-4',
|
|
263
263
|
])
|
|
@@ -280,7 +280,7 @@ const panelClass = computed(() => [
|
|
|
280
280
|
:aria-label="title"
|
|
281
281
|
:data-testid="testid ?? 'result-window'"
|
|
282
282
|
>
|
|
283
|
-
<header class="flex items-center gap-3 border-b border-
|
|
283
|
+
<header class="flex items-center gap-3 border-b border-default px-5 py-3">
|
|
284
284
|
<span
|
|
285
285
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
|
286
286
|
:class="iconClass"
|
|
@@ -288,8 +288,8 @@ const panelClass = computed(() => [
|
|
|
288
288
|
<UIcon :name="icon" class="h-4 w-4" />
|
|
289
289
|
</span>
|
|
290
290
|
<div class="min-w-0 flex-1">
|
|
291
|
-
<h2 class="truncate text-sm font-semibold text-
|
|
292
|
-
<p v-if="subtitle" class="truncate text-[11px] text-
|
|
291
|
+
<h2 class="truncate text-sm font-semibold text-app-100">{{ title }}</h2>
|
|
292
|
+
<p v-if="subtitle" class="truncate text-[11px] text-muted">{{ subtitle }}</p>
|
|
293
293
|
</div>
|
|
294
294
|
<!-- Window-specific header content (status badges, counts). -->
|
|
295
295
|
<slot name="header-extras" />
|
|
@@ -300,7 +300,7 @@ const panelClass = computed(() => [
|
|
|
300
300
|
@restarted="requestClose"
|
|
301
301
|
/>
|
|
302
302
|
<button
|
|
303
|
-
class="rounded-md p-1.5 text-
|
|
303
|
+
class="rounded-md p-1.5 text-muted hover:bg-elevated hover:text-default"
|
|
304
304
|
data-testid="result-window-close"
|
|
305
305
|
:aria-label="t('common.close')"
|
|
306
306
|
@click="requestClose"
|
|
@@ -318,18 +318,18 @@ const panelClass = computed(() => [
|
|
|
318
318
|
so it can't crowd a window out; expands in place. -->
|
|
319
319
|
<section
|
|
320
320
|
v-if="effortReport"
|
|
321
|
-
class="shrink-0 border-t border-
|
|
321
|
+
class="shrink-0 border-t border-default bg-default/60"
|
|
322
322
|
data-testid="result-window-effort"
|
|
323
323
|
>
|
|
324
324
|
<button
|
|
325
325
|
type="button"
|
|
326
|
-
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-
|
|
326
|
+
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-elevated/40"
|
|
327
327
|
:aria-expanded="effortOpen"
|
|
328
328
|
data-testid="result-window-effort-toggle"
|
|
329
329
|
@click="effortOpen = !effortOpen"
|
|
330
330
|
>
|
|
331
|
-
<UIcon name="i-lucide-gauge" class="h-3.5 w-3.5 shrink-0 text-
|
|
332
|
-
<span class="text-[11px] font-semibold uppercase tracking-wide text-
|
|
331
|
+
<UIcon name="i-lucide-gauge" class="h-3.5 w-3.5 shrink-0 text-muted" />
|
|
332
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-muted">
|
|
333
333
|
{{ t('panels.stepDetail.effort.heading') }}
|
|
334
334
|
</span>
|
|
335
335
|
<span
|
|
@@ -338,12 +338,12 @@ const panelClass = computed(() => [
|
|
|
338
338
|
>
|
|
339
339
|
{{ t('panels.stepDetail.effort.outOfTen', { value: effortReport.difficulty }) }}
|
|
340
340
|
</span>
|
|
341
|
-
<span v-if="hint" class="min-w-0 flex-1 truncate text-[12px] text-
|
|
341
|
+
<span v-if="hint" class="min-w-0 flex-1 truncate text-[12px] text-muted">
|
|
342
342
|
{{ hint }}
|
|
343
343
|
</span>
|
|
344
344
|
<UIcon
|
|
345
345
|
:name="effortOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
346
|
-
class="ms-auto h-3.5 w-3.5 shrink-0 text-
|
|
346
|
+
class="ms-auto h-3.5 w-3.5 shrink-0 text-dimmed"
|
|
347
347
|
/>
|
|
348
348
|
</button>
|
|
349
349
|
<div v-if="effortOpen" class="max-h-56 overflow-y-auto px-5 pb-3">
|
|
@@ -356,12 +356,12 @@ const panelClass = computed(() => [
|
|
|
356
356
|
red — a failed checkout is the one an operator opened the window to read. -->
|
|
357
357
|
<section
|
|
358
358
|
v-if="validationReport"
|
|
359
|
-
class="shrink-0 border-t border-
|
|
359
|
+
class="shrink-0 border-t border-default bg-default/60"
|
|
360
360
|
data-testid="result-window-validation"
|
|
361
361
|
>
|
|
362
362
|
<button
|
|
363
363
|
type="button"
|
|
364
|
-
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-
|
|
364
|
+
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-elevated/40"
|
|
365
365
|
:aria-expanded="validationOpen"
|
|
366
366
|
data-testid="result-window-validation-toggle"
|
|
367
367
|
@click="validationOpen = !validationOpen"
|
|
@@ -369,17 +369,17 @@ const panelClass = computed(() => [
|
|
|
369
369
|
<UIcon
|
|
370
370
|
:name="validationReport.passed ? 'i-lucide-shield-check' : 'i-lucide-shield-alert'"
|
|
371
371
|
class="h-3.5 w-3.5 shrink-0"
|
|
372
|
-
:class="validationReport.passed ? 'text-
|
|
372
|
+
:class="validationReport.passed ? 'text-app-success-400' : 'text-app-error-400'"
|
|
373
373
|
/>
|
|
374
|
-
<span class="text-[11px] font-semibold uppercase tracking-wide text-
|
|
374
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-muted">
|
|
375
375
|
{{ t('panels.stepDetail.validation.heading') }}
|
|
376
376
|
</span>
|
|
377
377
|
<span
|
|
378
378
|
class="shrink-0 rounded px-1.5 py-0.5 text-[11px] font-medium tabular-nums"
|
|
379
379
|
:class="
|
|
380
380
|
validationReport.passed
|
|
381
|
-
? 'bg-
|
|
382
|
-
: 'bg-
|
|
381
|
+
? 'bg-app-success-500/15 text-app-success-300'
|
|
382
|
+
: 'bg-app-error-500/15 text-app-error-300'
|
|
383
383
|
"
|
|
384
384
|
>
|
|
385
385
|
{{
|
|
@@ -388,7 +388,7 @@ const panelClass = computed(() => [
|
|
|
388
388
|
: t('panels.stepDetail.validation.failed')
|
|
389
389
|
}}
|
|
390
390
|
</span>
|
|
391
|
-
<span class="min-w-0 flex-1 truncate text-[12px] text-
|
|
391
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-muted">
|
|
392
392
|
{{
|
|
393
393
|
t('panels.stepDetail.validation.attempts', {
|
|
394
394
|
attempts: validationReport.attempts,
|
|
@@ -398,7 +398,7 @@ const panelClass = computed(() => [
|
|
|
398
398
|
</span>
|
|
399
399
|
<UIcon
|
|
400
400
|
:name="validationOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
401
|
-
class="ms-auto h-3.5 w-3.5 shrink-0 text-
|
|
401
|
+
class="ms-auto h-3.5 w-3.5 shrink-0 text-dimmed"
|
|
402
402
|
/>
|
|
403
403
|
</button>
|
|
404
404
|
<div v-if="validationOpen" class="max-h-72 overflow-y-auto px-5 pb-3">
|
|
@@ -412,12 +412,12 @@ const panelClass = computed(() => [
|
|
|
412
412
|
opened the window to read. -->
|
|
413
413
|
<section
|
|
414
414
|
v-if="reproductionReport"
|
|
415
|
-
class="shrink-0 border-t border-
|
|
415
|
+
class="shrink-0 border-t border-default bg-default/60"
|
|
416
416
|
data-testid="result-window-reproduction"
|
|
417
417
|
>
|
|
418
418
|
<button
|
|
419
419
|
type="button"
|
|
420
|
-
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-
|
|
420
|
+
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-elevated/40"
|
|
421
421
|
:aria-expanded="reproductionOpen"
|
|
422
422
|
data-testid="result-window-reproduction-toggle"
|
|
423
423
|
@click="reproductionOpen = !reproductionOpen"
|
|
@@ -425,27 +425,27 @@ const panelClass = computed(() => [
|
|
|
425
425
|
<UIcon
|
|
426
426
|
:name="reproductionKeys!.icon"
|
|
427
427
|
class="h-3.5 w-3.5 shrink-0"
|
|
428
|
-
:class="reproductionKeys!.proven ? 'text-
|
|
428
|
+
:class="reproductionKeys!.proven ? 'text-app-success-400' : 'text-app-warning-400'"
|
|
429
429
|
/>
|
|
430
|
-
<span class="text-[11px] font-semibold uppercase tracking-wide text-
|
|
430
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-muted">
|
|
431
431
|
{{ t('panels.stepDetail.reproduction.heading') }}
|
|
432
432
|
</span>
|
|
433
433
|
<span
|
|
434
434
|
class="shrink-0 rounded px-1.5 py-0.5 text-[11px] font-medium"
|
|
435
435
|
:class="
|
|
436
436
|
reproductionKeys!.proven
|
|
437
|
-
? 'bg-
|
|
438
|
-
: 'bg-
|
|
437
|
+
? 'bg-app-success-500/15 text-app-success-300'
|
|
438
|
+
: 'bg-app-warning-500/15 text-app-warning-300'
|
|
439
439
|
"
|
|
440
440
|
>
|
|
441
441
|
{{ t(reproductionKeys!.chip) }}
|
|
442
442
|
</span>
|
|
443
|
-
<span class="min-w-0 flex-1 truncate text-[12px] text-
|
|
443
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-muted">
|
|
444
444
|
{{ t(reproductionKeys!.verdict) }}
|
|
445
445
|
</span>
|
|
446
446
|
<UIcon
|
|
447
447
|
:name="reproductionOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
448
|
-
class="ms-auto h-3.5 w-3.5 shrink-0 text-
|
|
448
|
+
class="ms-auto h-3.5 w-3.5 shrink-0 text-dimmed"
|
|
449
449
|
/>
|
|
450
450
|
</button>
|
|
451
451
|
<div v-if="reproductionOpen" class="max-h-72 overflow-y-auto px-5 pb-3">
|
|
@@ -460,26 +460,26 @@ const panelClass = computed(() => [
|
|
|
460
460
|
not all read as. -->
|
|
461
461
|
<section
|
|
462
462
|
v-if="binaryOutputs"
|
|
463
|
-
class="shrink-0 border-t border-
|
|
463
|
+
class="shrink-0 border-t border-default bg-default/60"
|
|
464
464
|
data-testid="result-window-binary-outputs"
|
|
465
465
|
>
|
|
466
466
|
<button
|
|
467
467
|
type="button"
|
|
468
|
-
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-
|
|
468
|
+
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-elevated/40"
|
|
469
469
|
:aria-expanded="binaryOutputsOpen"
|
|
470
470
|
data-testid="result-window-binary-outputs-toggle"
|
|
471
471
|
@click="binaryOutputsOpen = !binaryOutputsOpen"
|
|
472
472
|
>
|
|
473
|
-
<UIcon name="i-lucide-image" class="h-3.5 w-3.5 shrink-0 text-
|
|
474
|
-
<span class="text-[11px] font-semibold uppercase tracking-wide text-
|
|
473
|
+
<UIcon name="i-lucide-image" class="h-3.5 w-3.5 shrink-0 text-muted" />
|
|
474
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-muted">
|
|
475
475
|
{{ t('binaryOutput.heading') }}
|
|
476
476
|
</span>
|
|
477
|
-
<span class="min-w-0 flex-1 truncate text-[12px] text-
|
|
477
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-muted">
|
|
478
478
|
{{ binaryOutputSummary }}
|
|
479
479
|
</span>
|
|
480
480
|
<UIcon
|
|
481
481
|
:name="binaryOutputsOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
482
|
-
class="ms-auto h-3.5 w-3.5 shrink-0 text-
|
|
482
|
+
class="ms-auto h-3.5 w-3.5 shrink-0 text-dimmed"
|
|
483
483
|
/>
|
|
484
484
|
</button>
|
|
485
485
|
<div v-if="binaryOutputsOpen && activeStep" class="max-h-72 overflow-y-auto px-5 pb-3">
|
|
@@ -25,8 +25,8 @@ const error = computed(() => execution.fullError(props.instanceId))
|
|
|
25
25
|
<template>
|
|
26
26
|
<div
|
|
27
27
|
v-if="loading || error"
|
|
28
|
-
class="flex items-center gap-2 border-b border-
|
|
29
|
-
:class="error ? 'text-
|
|
28
|
+
class="flex items-center gap-2 border-b border-default px-4 py-2 text-[11px]"
|
|
29
|
+
:class="error ? 'text-app-error-300' : 'text-muted'"
|
|
30
30
|
data-testid="run-detail-load-state"
|
|
31
31
|
>
|
|
32
32
|
<UIcon
|
|
@@ -51,22 +51,22 @@ const CONTAINER_STATUS_META: Record<
|
|
|
51
51
|
starting: {
|
|
52
52
|
icon: 'i-lucide-loader-circle',
|
|
53
53
|
spin: true,
|
|
54
|
-
cls: 'border-
|
|
54
|
+
cls: 'border-app-info-900/50 bg-app-info-950/30 text-app-info-300',
|
|
55
55
|
},
|
|
56
56
|
up: {
|
|
57
57
|
icon: 'i-lucide-box',
|
|
58
58
|
spin: false,
|
|
59
|
-
cls: 'border-
|
|
59
|
+
cls: 'border-app-success-900/50 bg-app-success-950/30 text-app-success-300',
|
|
60
60
|
},
|
|
61
61
|
errored: {
|
|
62
62
|
icon: 'i-lucide-circle-x',
|
|
63
63
|
spin: false,
|
|
64
|
-
cls: 'border-
|
|
64
|
+
cls: 'border-app-error-900/50 bg-app-error-950/30 text-app-error-300',
|
|
65
65
|
},
|
|
66
66
|
destroyed: {
|
|
67
67
|
icon: 'i-lucide-power-off',
|
|
68
68
|
spin: false,
|
|
69
|
-
cls: 'border-
|
|
69
|
+
cls: 'border-default bg-default/40 text-muted',
|
|
70
70
|
},
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -105,16 +105,16 @@ const { copy: copyText } = useCopyToClipboard()
|
|
|
105
105
|
/>
|
|
106
106
|
<span class="font-medium">{{ t(CONTAINER_STATUS_KEYS[containerStatus]) }}</span>
|
|
107
107
|
<template v-if="phaseLabel && containerStatus === 'up'">
|
|
108
|
-
<span class="text-
|
|
108
|
+
<span class="text-dimmed">·</span>
|
|
109
109
|
<span>{{ phaseLabel }}</span>
|
|
110
110
|
</template>
|
|
111
111
|
</div>
|
|
112
112
|
<dl v-if="step.container?.id || step.container?.url" class="mt-2 space-y-1">
|
|
113
113
|
<div v-if="step.container?.id" class="flex items-center gap-2">
|
|
114
|
-
<dt class="shrink-0 text-[11px] uppercase tracking-wide text-
|
|
114
|
+
<dt class="shrink-0 text-[11px] uppercase tracking-wide text-dimmed">
|
|
115
115
|
{{ t('panels.stepMeta.container.id') }}
|
|
116
116
|
</dt>
|
|
117
|
-
<dd class="truncate font-mono text-[11px] text-
|
|
117
|
+
<dd class="truncate font-mono text-[11px] text-toned" :title="step.container.id">
|
|
118
118
|
{{ step.container.id }}
|
|
119
119
|
</dd>
|
|
120
120
|
<UButton
|
|
@@ -129,10 +129,10 @@ const { copy: copyText } = useCopyToClipboard()
|
|
|
129
129
|
/>
|
|
130
130
|
</div>
|
|
131
131
|
<div v-if="step.container?.url" class="flex items-center gap-2">
|
|
132
|
-
<dt class="shrink-0 text-[11px] uppercase tracking-wide text-
|
|
132
|
+
<dt class="shrink-0 text-[11px] uppercase tracking-wide text-dimmed">
|
|
133
133
|
{{ t('panels.stepMeta.container.url') }}
|
|
134
134
|
</dt>
|
|
135
|
-
<dd class="truncate font-mono text-[11px] text-
|
|
135
|
+
<dd class="truncate font-mono text-[11px] text-toned">
|
|
136
136
|
<a
|
|
137
137
|
:href="step.container.url"
|
|
138
138
|
target="_blank"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
DELEGATION_STATUS_KEY,
|
|
4
|
+
DELEGATION_STATUS_META,
|
|
5
|
+
KNOWN_DELEGATION_STATUSES,
|
|
6
|
+
UNKNOWN_DELEGATION_STATUS_META,
|
|
7
|
+
delegatedUsageUnreported,
|
|
8
|
+
delegationStatusView,
|
|
9
|
+
externalRunHref,
|
|
10
|
+
} from './StepDelegatedStatus.logic'
|
|
11
|
+
import type { RunDelegationStatus } from '~/types/execution'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The status of external work is a CLOSED, PERSISTED vocabulary, which is the pairing that makes an
|
|
15
|
+
* exhaustive `Record` over it total against the TYPE and partial against the DATA. These pin the
|
|
16
|
+
* two ways that regresses: a member with no presentation, and a stored value this build does not
|
|
17
|
+
* know taking the whole panel down with it.
|
|
18
|
+
*/
|
|
19
|
+
describe('delegation status presentation', () => {
|
|
20
|
+
it('gives every status in the wire vocabulary its own row in both maps', () => {
|
|
21
|
+
// Derived from the schema the backend decides against, not from a list retyped here.
|
|
22
|
+
expect(Object.keys(DELEGATION_STATUS_META).sort()).toEqual(
|
|
23
|
+
[...KNOWN_DELEGATION_STATUSES].sort(),
|
|
24
|
+
)
|
|
25
|
+
expect(Object.keys(DELEGATION_STATUS_KEY).sort()).toEqual([...KNOWN_DELEGATION_STATUSES].sort())
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('never points two statuses at one label', () => {
|
|
29
|
+
const keys = Object.values(DELEGATION_STATUS_KEY)
|
|
30
|
+
expect(new Set(keys).size).toBe(keys.length)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('resolves a known status to its own row', () => {
|
|
34
|
+
const view = delegationStatusView('running')
|
|
35
|
+
expect(view.status).toBe('running')
|
|
36
|
+
expect(view.meta).toEqual(DELEGATION_STATUS_META.running)
|
|
37
|
+
expect(view.labelKey).toBe(DELEGATION_STATUS_KEY.running)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders a status this build does not know as unrecognised, NAMING the raw value', () => {
|
|
41
|
+
// The panel is the only place a person can read what happened to work the platform cannot see,
|
|
42
|
+
// so a bare `Record` lookup here is `undefined.cls` thrown during render: a white screen over
|
|
43
|
+
// the answer. Guessing onto a current member is the other wrong answer, because nothing can
|
|
44
|
+
// know which one was meant and the wrong guess reports the wrong outcome for a live run.
|
|
45
|
+
const view = delegationStatusView('quiesced')
|
|
46
|
+
expect(view.status).toBeNull()
|
|
47
|
+
expect(view.meta).toEqual(UNKNOWN_DELEGATION_STATUS_META)
|
|
48
|
+
expect(view.labelKey).toBe('panels.stepMeta.delegated.status.unrecognised')
|
|
49
|
+
expect(view.labelParams).toEqual({ status: 'quiesced' })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does not mistake an inherited object member for a status', () => {
|
|
53
|
+
// `DELEGATION_STATUS_META` is an ordinary object literal, so a persisted value naming
|
|
54
|
+
// `constructor` or `toString` reads back as a truthy non-key on a plain truthiness check.
|
|
55
|
+
for (const value of ['constructor', 'toString', 'hasOwnProperty']) {
|
|
56
|
+
expect(delegationStatusView(value).status).toBeNull()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('treats an absent status as unrecognised rather than throwing', () => {
|
|
61
|
+
expect(delegationStatusView(undefined).status).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('externalRunHref', () => {
|
|
66
|
+
it('links an ordinary run url', () => {
|
|
67
|
+
expect(externalRunHref('https://github.com/acme/widgets/actions/runs/99')).toBe(
|
|
68
|
+
'https://github.com/acme/widgets/actions/runs/99',
|
|
69
|
+
)
|
|
70
|
+
// http too: a deployment's own internal CI is reachable, and this is a link a person clicks,
|
|
71
|
+
// not a request the platform makes.
|
|
72
|
+
expect(externalRunHref('http://ci.internal/jobs/12')).toBe('http://ci.internal/jobs/12')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('refuses a scheme that would EXECUTE in this origin', () => {
|
|
76
|
+
// The value comes from an external system's API and lands in the card's primary affordance.
|
|
77
|
+
// Bound straight into `href`, these run as script in the SPA's own origin when clicked.
|
|
78
|
+
for (const url of ['javascript:alert(1)', 'JavaScript:alert(1)', 'data:text/html,<script>']) {
|
|
79
|
+
expect(externalRunHref(url)).toBeNull()
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('refuses what it cannot parse, and says nothing about a record with no url', () => {
|
|
84
|
+
expect(externalRunHref('/relative/path')).toBeNull()
|
|
85
|
+
expect(externalRunHref('')).toBeNull()
|
|
86
|
+
expect(externalRunHref(null)).toBeNull()
|
|
87
|
+
expect(externalRunHref(undefined)).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('delegatedUsageUnreported', () => {
|
|
92
|
+
// The two surfaces answering this question have to agree: the backend's reporting-gap fold
|
|
93
|
+
// counts the same settled steps into `delegatedStepsWithoutUsage`, and a card keyed on the
|
|
94
|
+
// executor's DECLARATION disagreed with it in both directions at once.
|
|
95
|
+
const step = (
|
|
96
|
+
status: RunDelegationStatus,
|
|
97
|
+
metrics?: { calls: number },
|
|
98
|
+
): Parameters<typeof delegatedUsageUnreported>[0] =>
|
|
99
|
+
({
|
|
100
|
+
delegated: {
|
|
101
|
+
executor: 'acme:executor',
|
|
102
|
+
status,
|
|
103
|
+
correlationKey: 'k',
|
|
104
|
+
poll: null,
|
|
105
|
+
attempts: [{ startedAt: 0 }],
|
|
106
|
+
},
|
|
107
|
+
...(metrics ? { metrics } : {}),
|
|
108
|
+
}) as Parameters<typeof delegatedUsageUnreported>[0]
|
|
109
|
+
|
|
110
|
+
it('warns on a SETTLED step that reported nothing, whatever the executor declared', () => {
|
|
111
|
+
expect(delegatedUsageUnreported(step('done'), { telemetry: 'self-reported' })).toBe(true)
|
|
112
|
+
expect(delegatedUsageUnreported(step('failed'), { telemetry: 'not-reported' })).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('stays silent beside a real number, even from an executor that declared none', () => {
|
|
116
|
+
expect(
|
|
117
|
+
delegatedUsageUnreported(step('done', { calls: 4 }), { telemetry: 'not-reported' }),
|
|
118
|
+
).toBe(false)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('warns while work is IN FLIGHT only where the declaration says none is coming', () => {
|
|
122
|
+
// A `self-reported` executor files with its result, so it has correctly reported nothing yet
|
|
123
|
+
// and the card must not claim a permanent gap while a person watches the step.
|
|
124
|
+
expect(delegatedUsageUnreported(step('running'), { telemetry: 'self-reported' })).toBe(false)
|
|
125
|
+
expect(delegatedUsageUnreported(step('running'), { telemetry: 'not-reported' })).toBe(true)
|
|
126
|
+
// An executor this build no longer registers: nothing here can say otherwise.
|
|
127
|
+
expect(delegatedUsageUnreported(step('starting'), undefined)).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('says nothing at all about a step that dispatched nowhere external', () => {
|
|
131
|
+
expect(
|
|
132
|
+
delegatedUsageUnreported({} as Parameters<typeof delegatedUsageUnreported>[0], undefined),
|
|
133
|
+
).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
delegatedSpendUnreported,
|
|
3
|
+
isRunDelegationStatus,
|
|
4
|
+
runDelegationStatusSchema,
|
|
5
|
+
} from '@cat-factory/contracts'
|
|
6
|
+
import type { PipelineStep, RunDelegationStatus } from '~/types/execution'
|
|
7
|
+
|
|
8
|
+
// The DELEGATION status vocabulary's presentation. Kept out of the SFC on the same seam as
|
|
9
|
+
// `StepToolServers.logic.ts`, so the rule can be asserted without mounting a component.
|
|
10
|
+
//
|
|
11
|
+
// The rule: the status of external work is a CLOSED and PERSISTED vocabulary, so a row written by
|
|
12
|
+
// another build can hold a member this one retired. Every `Record<RunDelegationStatus, …>` over it
|
|
13
|
+
// is therefore total against the TYPE and partial against the DATA, and indexing one bare is
|
|
14
|
+
// `undefined.cls` thrown during render: a white screen over the one panel that says what happened
|
|
15
|
+
// to work the platform cannot see. A value this build does not know is RENDERED as unrecognised,
|
|
16
|
+
// carrying the raw string, never guessed onto a current member: nothing can know which one was
|
|
17
|
+
// meant, and the wrong guess reports the wrong outcome for a run somebody may need to go and stop.
|
|
18
|
+
|
|
19
|
+
/** How each status renders. Exhaustive, so a member added to the vocabulary fails to compile here. */
|
|
20
|
+
export const DELEGATION_STATUS_META: Record<
|
|
21
|
+
RunDelegationStatus,
|
|
22
|
+
{ icon: string; spin: boolean; cls: string }
|
|
23
|
+
> = {
|
|
24
|
+
starting: {
|
|
25
|
+
icon: 'i-lucide-loader-circle',
|
|
26
|
+
spin: true,
|
|
27
|
+
cls: 'border-app-info-900/50 bg-app-info-950/30 text-app-info-300',
|
|
28
|
+
},
|
|
29
|
+
running: {
|
|
30
|
+
icon: 'i-lucide-radio',
|
|
31
|
+
spin: false,
|
|
32
|
+
cls: 'border-primary/50 bg-primary/10 text-primary',
|
|
33
|
+
},
|
|
34
|
+
done: {
|
|
35
|
+
icon: 'i-lucide-circle-check',
|
|
36
|
+
spin: false,
|
|
37
|
+
cls: 'border-app-success-900/50 bg-app-success-950/30 text-app-success-300',
|
|
38
|
+
},
|
|
39
|
+
failed: {
|
|
40
|
+
icon: 'i-lucide-circle-x',
|
|
41
|
+
spin: false,
|
|
42
|
+
cls: 'border-app-error-900/50 bg-app-error-950/30 text-app-error-300',
|
|
43
|
+
},
|
|
44
|
+
cancelled: {
|
|
45
|
+
icon: 'i-lucide-circle-slash',
|
|
46
|
+
spin: false,
|
|
47
|
+
cls: 'border-app-warning-900/50 bg-app-warning-950/30 text-app-warning-300',
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Static literal keys (not a runtime-built `t(`…${status}`)`) so the typed-message-keys check
|
|
53
|
+
* covers them; exhaustive on the same vocabulary and for the same reason as
|
|
54
|
+
* {@link DELEGATION_STATUS_META}.
|
|
55
|
+
*/
|
|
56
|
+
export const DELEGATION_STATUS_KEY: Record<RunDelegationStatus, string> = {
|
|
57
|
+
starting: 'panels.stepMeta.delegated.status.starting',
|
|
58
|
+
running: 'panels.stepMeta.delegated.status.running',
|
|
59
|
+
done: 'panels.stepMeta.delegated.status.done',
|
|
60
|
+
failed: 'panels.stepMeta.delegated.status.failed',
|
|
61
|
+
cancelled: 'panels.stepMeta.delegated.status.cancelled',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* How a status this build does not recognise renders: the WARNING tone, like `cancelled`, because
|
|
66
|
+
* both mean "something here wants a person to look". Its own row rather than a borrowed one, so
|
|
67
|
+
* nothing reads it as one of the known outcomes.
|
|
68
|
+
*/
|
|
69
|
+
export const UNKNOWN_DELEGATION_STATUS_META = {
|
|
70
|
+
icon: 'i-lucide-circle-help',
|
|
71
|
+
spin: false,
|
|
72
|
+
cls: 'border-app-warning-900/50 bg-app-warning-950/30 text-app-warning-300',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The vocabulary as the SCHEMA states it: what a parity assertion grades the two maps against. */
|
|
76
|
+
export const KNOWN_DELEGATION_STATUSES = runDelegationStatusSchema.options
|
|
77
|
+
|
|
78
|
+
/** What the card renders for one persisted status, known or not. */
|
|
79
|
+
export interface DelegationStatusView {
|
|
80
|
+
/** The narrowed member, or null for a value this build does not know. */
|
|
81
|
+
status: RunDelegationStatus | null
|
|
82
|
+
meta: { icon: string; spin: boolean; cls: string }
|
|
83
|
+
/** The i18n key plus the parameters it takes; the raw value rides the unrecognised one. */
|
|
84
|
+
labelKey: string
|
|
85
|
+
labelParams: Record<string, string>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve one persisted status into everything the card shows for it.
|
|
90
|
+
*
|
|
91
|
+
* One function rather than three guarded lookups in the template, because the guard is the whole
|
|
92
|
+
* point: three places to narrow is three places to forget, and forgetting once throws.
|
|
93
|
+
*/
|
|
94
|
+
export function delegationStatusView(status: string | undefined): DelegationStatusView {
|
|
95
|
+
if (status && isRunDelegationStatus(status)) {
|
|
96
|
+
return {
|
|
97
|
+
status,
|
|
98
|
+
meta: DELEGATION_STATUS_META[status],
|
|
99
|
+
labelKey: DELEGATION_STATUS_KEY[status],
|
|
100
|
+
labelParams: {},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
status: null,
|
|
105
|
+
meta: UNKNOWN_DELEGATION_STATUS_META,
|
|
106
|
+
labelKey: 'panels.stepMeta.delegated.status.unrecognised',
|
|
107
|
+
labelParams: { status: status ?? '' },
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The run URL as an `href`, or null when it is not one this SPA may link to.
|
|
113
|
+
*
|
|
114
|
+
* Everything on the delegation record comes from an EXTERNAL system: `url` is whatever that
|
|
115
|
+
* system's API answered with, and this card puts it in the primary affordance a person clicks.
|
|
116
|
+
* Bound straight into `href`, a `javascript:` value executes in the SPA's own origin, which is the
|
|
117
|
+
* same boundary `resolveExternalToolUrl` draws for a resolver-supplied URL and the same one
|
|
118
|
+
* kernel's `hostMarkdown.link` draws server-side.
|
|
119
|
+
*
|
|
120
|
+
* A refused value is not dropped: the caller renders it as plain text, because "the executor
|
|
121
|
+
* reported this link and we will not follow it" is a fact worth showing, and a silently missing
|
|
122
|
+
* link reads as a run that reported none.
|
|
123
|
+
*/
|
|
124
|
+
export function externalRunHref(url: string | null | undefined): string | null {
|
|
125
|
+
if (!url) return null
|
|
126
|
+
try {
|
|
127
|
+
const parsed = new URL(url)
|
|
128
|
+
return parsed.protocol === 'https:' || parsed.protocol === 'http:' ? url : null
|
|
129
|
+
} catch {
|
|
130
|
+
// A relative or malformed value: not something to link to, and not an error either. The card
|
|
131
|
+
// shows the raw string, which is what the executor actually said.
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether the card says the platform is not measuring this step's spend.
|
|
138
|
+
*
|
|
139
|
+
* Two questions, and the split is the whole point. Once the work has SETTLED the answer is
|
|
140
|
+
* contracts' `delegatedSpendUnreported`, over what actually LANDED, which is the same rule the
|
|
141
|
+
* backend's reporting-gap fold reads: keying this side on the executor's declared `telemetry`
|
|
142
|
+
* instead made the two disagree in both directions at once, an executor declaring `self-reported`
|
|
143
|
+
* that files nothing showing no warning here while the debug overview counted the step, and one
|
|
144
|
+
* declaring `not-reported` that does fill `DelegationResult.usage` printing "not reported" beside
|
|
145
|
+
* a real number.
|
|
146
|
+
*
|
|
147
|
+
* While the work is IN FLIGHT only the declaration can say anything, because a `self-reported`
|
|
148
|
+
* executor files with its result and has correctly reported nothing yet. So the line is shown for
|
|
149
|
+
* one that declares the number is never coming, and for one this build no longer registers, where
|
|
150
|
+
* nothing here can say otherwise.
|
|
151
|
+
*/
|
|
152
|
+
export function delegatedUsageUnreported(
|
|
153
|
+
step: Pick<PipelineStep, 'delegated' | 'metrics'>,
|
|
154
|
+
executor: { telemetry?: 'not-reported' | 'self-reported' } | undefined,
|
|
155
|
+
): boolean {
|
|
156
|
+
if (delegatedSpendUnreported(step)) return true
|
|
157
|
+
const status = step.delegated?.status
|
|
158
|
+
const inFlight = status === 'starting' || status === 'running'
|
|
159
|
+
return inFlight && executor?.telemetry !== 'self-reported'
|
|
160
|
+
}
|