@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.
Files changed (328) hide show
  1. package/README.md +16 -0
  2. package/app/app.config.ts +19 -10
  3. package/app/assets/css/main.css +13 -13
  4. package/app/assets/css/prose.css +22 -22
  5. package/app/assets/css/tokens.css +298 -0
  6. package/app/components/assistant/AssistantModal.vue +22 -22
  7. package/app/components/auth/AuthGate.vue +1 -1
  8. package/app/components/auth/BackendMisconfiguredScreen.vue +10 -10
  9. package/app/components/auth/LoginScreen.vue +34 -36
  10. package/app/components/auth/ResetPasswordScreen.vue +12 -12
  11. package/app/components/auth/UserMenu.vue +4 -4
  12. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +27 -25
  13. package/app/components/binaryOutput/BinaryOutputReport.vue +15 -15
  14. package/app/components/binaryOutput/StoredAssetView.vue +5 -5
  15. package/app/components/board/AddTaskModal.vue +17 -20
  16. package/app/components/board/AgentFailureCard.vue +11 -11
  17. package/app/components/board/AgentFailureHistory.vue +2 -2
  18. package/app/components/board/BoardCanvas.vue +6 -5
  19. package/app/components/board/CreateInitiativeModal.vue +8 -10
  20. package/app/components/board/DependencyConnectOverlay.vue +1 -1
  21. package/app/components/board/FailureHistoryList.vue +7 -7
  22. package/app/components/board/RecurringPipelineModal.vue +17 -17
  23. package/app/components/board/ReviewFrictionDialog.vue +6 -6
  24. package/app/components/board/StepExecutionHistory.vue +13 -13
  25. package/app/components/board/TaskDependencyEdges.vue +8 -8
  26. package/app/components/board/nodes/AgentChip.vue +10 -10
  27. package/app/components/board/nodes/BlockNode.vue +29 -29
  28. package/app/components/board/nodes/DecisionBadge.vue +2 -2
  29. package/app/components/board/nodes/EpicNode.vue +14 -10
  30. package/app/components/board/nodes/FrameSwimlanes.vue +7 -9
  31. package/app/components/board/nodes/InitiativeCard.vue +8 -8
  32. package/app/components/board/nodes/LaneGroup.vue +1 -1
  33. package/app/components/board/nodes/LaneTask.vue +2 -2
  34. package/app/components/board/nodes/ResizeGrips.vue +4 -4
  35. package/app/components/board/nodes/TaskCard.vue +18 -18
  36. package/app/components/board/nodes/TaskLane.vue +5 -5
  37. package/app/components/board/nodes/TaskPipelineMini.vue +18 -18
  38. package/app/components/bootstrap/AdoptionReviewModal.vue +29 -26
  39. package/app/components/bootstrap/BootstrapModal.vue +21 -23
  40. package/app/components/bootstrap/BootstrapRunSteps.vue +1 -1
  41. package/app/components/brainstorm/BrainstormWindow.vue +30 -33
  42. package/app/components/bugFishing/BugFishingWindow.vue +49 -49
  43. package/app/components/clarity/ClarityReviewWindow.vue +30 -30
  44. package/app/components/common/AsyncViewError.vue +5 -5
  45. package/app/components/common/ClarificationItem.vue +8 -13
  46. package/app/components/common/ConfirmDialog.vue +4 -2
  47. package/app/components/common/DescriptorFields.vue +1 -1
  48. package/app/components/common/EmptyState.vue +3 -3
  49. package/app/components/common/InterviewGateNotice.vue +7 -7
  50. package/app/components/common/KeyboardShortcutsHelp.vue +1 -1
  51. package/app/components/common/MarkdownProse.vue +14 -13
  52. package/app/components/consensus/ConsensusSessionWindow.vue +26 -26
  53. package/app/components/context/ContextAttachmentFields.vue +17 -17
  54. package/app/components/docs/DocInterviewWindow.vue +14 -14
  55. package/app/components/documents/ContextDocumentPicker.vue +17 -17
  56. package/app/components/documents/DocumentImportModal.vue +7 -7
  57. package/app/components/documents/DocumentSourceConnectModal.vue +3 -3
  58. package/app/components/documents/DocumentSyncState.vue +5 -5
  59. package/app/components/documents/DocumentTemplatesModal.vue +15 -15
  60. package/app/components/documents/RepoContextDocPicker.vue +5 -5
  61. package/app/components/documents/SpawnPreviewModal.vue +12 -12
  62. package/app/components/documents/StartFromDesignModal.vue +11 -11
  63. package/app/components/documents/TaskContextDocs.vue +5 -5
  64. package/app/components/environments/EnvSetupStepper.vue +6 -6
  65. package/app/components/environments/EnvironmentStatusPanel.vue +17 -17
  66. package/app/components/environments/steps/EnvPickStep.vue +3 -3
  67. package/app/components/environments/steps/EnvPreflightStep.vue +9 -9
  68. package/app/components/environments/steps/EnvReviewStep.vue +25 -22
  69. package/app/components/environments/steps/EnvSaveStep.vue +7 -5
  70. package/app/components/environments/steps/JourneyStepNav.vue +1 -1
  71. package/app/components/focus/BlockFocusView.vue +17 -19
  72. package/app/components/followUp/FollowUpWindow.vue +23 -23
  73. package/app/components/forkDecision/ForkDecisionWindow.vue +37 -39
  74. package/app/components/foundational/FoundationalContractSummary.vue +7 -7
  75. package/app/components/foundational/FoundationalServiceCatalogList.vue +11 -11
  76. package/app/components/foundational/FoundationalServiceManager.vue +1 -1
  77. package/app/components/foundational/FoundationalServiceRegistry.vue +13 -13
  78. package/app/components/foundational/FoundationalServiceSources.vue +13 -13
  79. package/app/components/foundational/FoundationalSuppressions.vue +7 -7
  80. package/app/components/foundational/ServiceCatalogConnection.vue +10 -10
  81. package/app/components/fragments/FragmentLibraryManager.vue +41 -44
  82. package/app/components/fragments/FragmentSelector.vue +13 -13
  83. package/app/components/fragments/GitHubDocUrlImport.vue +1 -1
  84. package/app/components/gates/GateFailingCheckList.vue +5 -5
  85. package/app/components/gates/GateResultView.vue +50 -47
  86. package/app/components/github/AddServiceFromRepoModal.vue +14 -14
  87. package/app/components/github/BranchProtectionPreflight.vue +13 -13
  88. package/app/components/github/GitHubConnect.vue +7 -7
  89. package/app/components/github/GitHubOnboarding.vue +7 -7
  90. package/app/components/github/GitHubPanel.vue +37 -40
  91. package/app/components/github/RepoTreeBrowser.vue +18 -18
  92. package/app/components/humanTest/HumanTestWindow.vue +30 -33
  93. package/app/components/initiative/InitiativePlanNotice.vue +5 -5
  94. package/app/components/initiative/InitiativePlanReview.vue +27 -29
  95. package/app/components/initiative/InitiativePlanningWindow.vue +10 -10
  96. package/app/components/initiative/InitiativeTrackerWindow.vue +71 -65
  97. package/app/components/inputGate/InputGateNotice.vue +2 -4
  98. package/app/components/judge/JudgeResultView.vue +30 -32
  99. package/app/components/kaizen/KaizenPanel.vue +40 -36
  100. package/app/components/kaizen/KaizenStepStatus.vue +21 -21
  101. package/app/components/layout/AccountAuditLog.vue +10 -10
  102. package/app/components/layout/AccountDeploymentSettings.vue +20 -20
  103. package/app/components/layout/AccountFailureKindRules.vue +13 -13
  104. package/app/components/layout/AccountFoundationalSettings.vue +1 -1
  105. package/app/components/layout/AccountFragmentSettings.vue +1 -1
  106. package/app/components/layout/AccountModelPolicySettings.vue +9 -9
  107. package/app/components/layout/AccountPlatformAlertSettings.vue +11 -11
  108. package/app/components/layout/AccountRiskPolicySettings.vue +2 -2
  109. package/app/components/layout/AccountRunCredentialSettings.vue +5 -5
  110. package/app/components/layout/AccountSkillSettings.vue +1 -1
  111. package/app/components/layout/AccountTeamSettings.vue +23 -15
  112. package/app/components/layout/AiProvidersBanner.vue +11 -8
  113. package/app/components/layout/AppearanceSwitcher.vue +108 -0
  114. package/app/components/layout/BoardSwitcher.vue +8 -11
  115. package/app/components/layout/BoardToolbar.vue +3 -3
  116. package/app/components/layout/CommandBar.vue +7 -9
  117. package/app/components/layout/ConnectionStatusBanner.vue +2 -2
  118. package/app/components/layout/DefaultTestEnvBanner.vue +5 -5
  119. package/app/components/layout/GitHubPatBanner.vue +5 -5
  120. package/app/components/layout/GitHubPatPermissionsBanner.vue +16 -10
  121. package/app/components/layout/InfraSetupBanner.vue +6 -6
  122. package/app/components/layout/IntegrationsHub.vue +15 -15
  123. package/app/components/layout/LanguageSwitcher.vue +5 -5
  124. package/app/components/layout/ModelProvidersHub.vue +13 -16
  125. package/app/components/layout/NotificationsInbox.vue +10 -10
  126. package/app/components/layout/PersonalSetupModal.vue +8 -8
  127. package/app/components/layout/ProviderConfigBanner.vue +4 -4
  128. package/app/components/layout/RolePrompt.vue +7 -7
  129. package/app/components/layout/SideBar.vue +6 -4
  130. package/app/components/layout/SpendWarningBanner.vue +17 -14
  131. package/app/components/layout/TranslationWarningBanner.vue +4 -4
  132. package/app/components/layout/UiModeSwitcher.vue +13 -13
  133. package/app/components/layout/UiRoleSwitcher.vue +8 -8
  134. package/app/components/layout/WorkspaceMembersSettings.vue +15 -11
  135. package/app/components/media/ArtifactLightbox.vue +13 -13
  136. package/app/components/media/ImageCompare.vue +31 -24
  137. package/app/components/merge/MergeEffortChips.vue +2 -2
  138. package/app/components/notifications/NotificationSettingsPanel.vue +14 -12
  139. package/app/components/observability/OutcomeFilterChips.vue +6 -6
  140. package/app/components/observability/RunFailureSummary.vue +28 -25
  141. package/app/components/observability/StepMetricsBar.vue +26 -28
  142. package/app/components/observability/StepModelActivity.vue +2 -2
  143. package/app/components/observability/ToolCallList.vue +24 -24
  144. package/app/components/outcome/OutcomeSummaryWindow.vue +47 -47
  145. package/app/components/palettes/AgentPalette.vue +8 -8
  146. package/app/components/palettes/AgentTierSelect.vue +1 -1
  147. package/app/components/palettes/PipelinePurposeSelect.vue +1 -1
  148. package/app/components/panels/AgentStepDetail.vue +52 -51
  149. package/app/components/panels/AttemptEntryHeader.vue +4 -6
  150. package/app/components/panels/DecisionModal.vue +7 -7
  151. package/app/components/panels/GenericStructuredResultView.vue +7 -7
  152. package/app/components/panels/InspectorPanel.vue +17 -17
  153. package/app/components/panels/MergerResultView.vue +19 -19
  154. package/app/components/panels/ObservabilityPanel.vue +115 -123
  155. package/app/components/panels/OperatorDashboardPanel.vue +89 -97
  156. package/app/components/panels/ReportsPanel.vue +69 -66
  157. package/app/components/panels/ReportsSpendBreakdown.vue +9 -9
  158. package/app/components/panels/ResultWindowShell.vue +38 -38
  159. package/app/components/panels/RunDetailLoadState.vue +2 -2
  160. package/app/components/panels/StepContainerStatus.vue +9 -9
  161. package/app/components/panels/StepDelegatedStatus.logic.spec.ts +135 -0
  162. package/app/components/panels/StepDelegatedStatus.logic.ts +160 -0
  163. package/app/components/panels/StepDelegatedStatus.vue +181 -0
  164. package/app/components/panels/StepEffortReport.vue +16 -15
  165. package/app/components/panels/StepFragmentAdherence.vue +14 -10
  166. package/app/components/panels/StepMetadataCard.vue +74 -45
  167. package/app/components/panels/StepReproductionReport.vue +25 -21
  168. package/app/components/panels/StepRunMeta.vue +20 -20
  169. package/app/components/panels/StepTestReport.logic.spec.ts +8 -3
  170. package/app/components/panels/StepTestReport.logic.ts +4 -4
  171. package/app/components/panels/StepTestReport.vue +28 -28
  172. package/app/components/panels/StepToolServers.vue +14 -11
  173. package/app/components/panels/StepValidationReport.vue +7 -7
  174. package/app/components/panels/inspector/ContainerSummary.vue +9 -9
  175. package/app/components/panels/inspector/DocReferenceRepos.vue +3 -3
  176. package/app/components/panels/inspector/EnvProbeReport.vue +24 -27
  177. package/app/components/panels/inspector/EpicChildren.vue +8 -12
  178. package/app/components/panels/inspector/FrontendBindingsResolved.vue +11 -11
  179. package/app/components/panels/inspector/FrontendConfig.vue +37 -41
  180. package/app/components/panels/inspector/InitiativeInspector.vue +3 -3
  181. package/app/components/panels/inspector/InspectorSection.vue +6 -6
  182. package/app/components/panels/inspector/RecurringScheduleSettings.vue +16 -19
  183. package/app/components/panels/inspector/ServiceConnections.vue +2 -2
  184. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -2
  185. package/app/components/panels/inspector/ServiceSelfTests.vue +21 -17
  186. package/app/components/panels/inspector/ServiceTestConfig.vue +51 -59
  187. package/app/components/panels/inspector/ServiceTestSecrets.vue +5 -5
  188. package/app/components/panels/inspector/ServiceTestingContext.vue +3 -3
  189. package/app/components/panels/inspector/ServiceValidationConfig.vue +2 -2
  190. package/app/components/panels/inspector/TaskAgentConfig.vue +3 -3
  191. package/app/components/panels/inspector/TaskAprioriBranches.vue +8 -5
  192. package/app/components/panels/inspector/TaskDependencies.vue +1 -1
  193. package/app/components/panels/inspector/TaskEstimateBadge.vue +11 -11
  194. package/app/components/panels/inspector/TaskExecution.vue +17 -17
  195. package/app/components/panels/inspector/TaskReviewTarget.vue +2 -2
  196. package/app/components/panels/inspector/TaskRunSettings.vue +35 -37
  197. package/app/components/panels/inspector/TaskStructure.vue +3 -3
  198. package/app/components/pipeline/AgentKindIcon.vue +1 -1
  199. package/app/components/pipeline/AgentPromptEditor.vue +17 -17
  200. package/app/components/pipeline/BinaryOutputStepPicker.vue +42 -56
  201. package/app/components/pipeline/EstimateThresholdFields.vue +4 -4
  202. package/app/components/pipeline/GateConfigFields.vue +13 -10
  203. package/app/components/pipeline/IterationCapPrompt.vue +4 -2
  204. package/app/components/pipeline/PipelineBuilder.vue +47 -55
  205. package/app/components/pipeline/PipelineHealthModal.vue +27 -29
  206. package/app/components/pipeline/PipelinePicker.vue +10 -10
  207. package/app/components/pipeline/PipelinePreview.vue +10 -10
  208. package/app/components/pipeline/PipelineProgress.vue +64 -67
  209. package/app/components/prReview/PrReviewPhaseBadge.vue +7 -7
  210. package/app/components/prReview/PrReviewWindow.vue +75 -77
  211. package/app/components/providers/AiPresetMismatchDialog.vue +5 -5
  212. package/app/components/providers/AiProviderOnboardingModal.vue +9 -9
  213. package/app/components/providers/ApiKeysSection.vue +13 -13
  214. package/app/components/providers/PersonalCredentialModal.vue +2 -2
  215. package/app/components/providers/PersonalSubscriptionSection.vue +10 -10
  216. package/app/components/providers/SignInRequiredNotice.vue +1 -1
  217. package/app/components/providers/VendorCredentialsModal.vue +10 -10
  218. package/app/components/provisioning/ProvisioningLogsDrawer.vue +13 -13
  219. package/app/components/ralph/RalphLoopResultView.vue +28 -30
  220. package/app/components/recurring/RecurrenceEditor.vue +2 -2
  221. package/app/components/requirements/RequirementsReviewWindow.vue +51 -58
  222. package/app/components/riskPolicy/RiskPolicyPicker.vue +12 -15
  223. package/app/components/riskPolicy/RiskPolicyPreview.vue +14 -14
  224. package/app/components/sandbox/SandboxPanel.vue +58 -59
  225. package/app/components/settings/AccountSettingsPanel.vue +1 -1
  226. package/app/components/settings/ApiTokensPanel.vue +10 -10
  227. package/app/components/settings/BudgetSettings.vue +18 -22
  228. package/app/components/settings/CapabilityCredentialsPanel.vue +15 -15
  229. package/app/components/settings/CloudflareHandlerSection.vue +20 -20
  230. package/app/components/settings/ComposeEnvironmentSetupSection.vue +13 -13
  231. package/app/components/settings/ConnectionTestVerdict.vue +3 -3
  232. package/app/components/settings/ConnectionWarnings.vue +1 -1
  233. package/app/components/settings/ConsensusGroupsSection.vue +27 -35
  234. package/app/components/settings/CustomManifestTypeEditor.vue +10 -10
  235. package/app/components/settings/DefaultProvisionTypeSection.vue +6 -6
  236. package/app/components/settings/InfraHandlersConfigurator.vue +24 -22
  237. package/app/components/settings/InfrastructureBackendPicker.vue +6 -6
  238. package/app/components/settings/InfrastructureWindow.vue +5 -5
  239. package/app/components/settings/IssueTrackerPanel.vue +29 -23
  240. package/app/components/settings/KubernetesEngineForm.vue +11 -11
  241. package/app/components/settings/KubernetesEnvironmentForm.vue +4 -4
  242. package/app/components/settings/LocalContainerPoolSettings.vue +6 -11
  243. package/app/components/settings/LocalModelEndpointsPanel.vue +20 -18
  244. package/app/components/settings/McpAuthorizeScreen.vue +13 -15
  245. package/app/components/settings/McpOAuthCallbackScreen.vue +16 -10
  246. package/app/components/settings/MergeClassRulesEditor.vue +6 -9
  247. package/app/components/settings/MergeRolePolicyEditor.vue +16 -16
  248. package/app/components/settings/ModelConfigurationPanel.vue +23 -30
  249. package/app/components/settings/ModelPresetHealthModal.vue +15 -13
  250. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -4
  251. package/app/components/settings/OpenRouterCatalogPanel.vue +23 -19
  252. package/app/components/settings/PackageRegistriesPanel.vue +8 -8
  253. package/app/components/settings/ProviderConnectionTab.vue +8 -11
  254. package/app/components/settings/ProviderManifestEditor.vue +9 -9
  255. package/app/components/settings/ProviderPreferenceEditor.vue +8 -8
  256. package/app/components/settings/RiskPolicyCreateForm.vue +10 -10
  257. package/app/components/settings/RiskPolicyEditorRow.vue +14 -14
  258. package/app/components/settings/RiskPolicyHealthModal.vue +15 -13
  259. package/app/components/settings/RiskPolicyInheritedRow.vue +1 -1
  260. package/app/components/settings/RiskPolicyPanel.vue +10 -15
  261. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +7 -7
  262. package/app/components/settings/SharedStacksPanel.vue +9 -12
  263. package/app/components/settings/TaskSourceCard.vue +4 -4
  264. package/app/components/settings/TaskTypeSuppressionsPanel.vue +6 -6
  265. package/app/components/settings/ToolServerChecklist.vue +36 -29
  266. package/app/components/settings/UsageSettings.vue +15 -15
  267. package/app/components/settings/UserSecretsSection.vue +9 -12
  268. package/app/components/settings/WorkspaceMetadataSettings.vue +5 -5
  269. package/app/components/settings/WorkspaceSettingsPanel.vue +47 -47
  270. package/app/components/skills/ReviewSkillQueue.vue +10 -10
  271. package/app/components/skills/SkillLibraryManager.vue +23 -20
  272. package/app/components/slack/SlackPanel.vue +13 -17
  273. package/app/components/spec/ServiceSpecWindow.vue +45 -42
  274. package/app/components/tasks/BugHuntModal.vue +24 -27
  275. package/app/components/tasks/ContextIssuePicker.vue +11 -11
  276. package/app/components/tasks/TaskContextIssues.vue +3 -3
  277. package/app/components/tasks/TaskImportModal.vue +7 -7
  278. package/app/components/tasks/TaskSourceConnectModal.vue +10 -8
  279. package/app/components/testing/TestReportWindow.vue +99 -82
  280. package/app/components/theme/ThemeImportModal.vue +115 -0
  281. package/app/components/tutorial/TutorialCatalogue.vue +9 -9
  282. package/app/components/tutorial/TutorialNudge.vue +5 -5
  283. package/app/components/tutorial/TutorialOverlay.vue +12 -12
  284. package/app/components/tutorial/TutorialPrompt.vue +6 -9
  285. package/app/components/vcs/GitLabConnect.vue +4 -4
  286. package/app/components/vcs/VcsConnectSurfaces.vue +2 -2
  287. package/app/components/visualConfirm/VisualConfirmationWindow.vue +36 -36
  288. package/app/composables/api/client.ts +6 -1
  289. package/app/composables/usePipelineErrorToast.ts +17 -0
  290. package/app/modular/agent-kinds.ts +24 -0
  291. package/app/pages/index.vue +5 -5
  292. package/app/plugins/theme.client.ts +125 -0
  293. package/app/spa-loading-template.html +52 -14
  294. package/app/stores/agents.ts +1 -1
  295. package/app/stores/theme.spec.ts +56 -0
  296. package/app/stores/theme.ts +87 -0
  297. package/app/types/domain.ts +19 -0
  298. package/app/types/execution.ts +4 -0
  299. package/app/utils/agentOutput.ts +1 -1
  300. package/app/utils/binaryOutput.ts +6 -6
  301. package/app/utils/bootstrapSteps.ts +14 -14
  302. package/app/utils/catalog.ts +87 -83
  303. package/app/utils/colorTint.ts +10 -0
  304. package/app/utils/failureKinds.ts +1 -0
  305. package/app/utils/observability.ts +3 -3
  306. package/app/utils/pipelineRender.ts +13 -17
  307. package/app/utils/swimlanes.ts +4 -4
  308. package/app/utils/theme/bootPalette.spec.ts +54 -0
  309. package/app/utils/theme/bootPalette.ts +108 -0
  310. package/app/utils/theme/builtins.spec.ts +38 -0
  311. package/app/utils/theme/builtins.ts +55 -0
  312. package/app/utils/theme/css.ts +162 -0
  313. package/app/utils/theme/doc.ts +353 -0
  314. package/app/utils/theme/link.ts +93 -0
  315. package/app/utils/theme/presets.ts +509 -0
  316. package/app/utils/theme/theme.spec.ts +224 -0
  317. package/i18n/locales/de.json +61 -4
  318. package/i18n/locales/en.json +70 -4
  319. package/i18n/locales/es.json +61 -4
  320. package/i18n/locales/fr.json +61 -4
  321. package/i18n/locales/he.json +61 -4
  322. package/i18n/locales/it.json +61 -4
  323. package/i18n/locales/ja.json +61 -4
  324. package/i18n/locales/pl.json +61 -4
  325. package/i18n/locales/tr.json +61 -4
  326. package/i18n/locales/uk.json +61 -4
  327. package/nuxt.config.ts +37 -11
  328. 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-slate-500/15 text-slate-300',
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-emerald-500/15 text-emerald-300',
248
- moderate: 'bg-amber-500/15 text-amber-300',
249
- hard: 'bg-rose-500/15 text-rose-300',
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-slate-950/70 backdrop-blur-sm',
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-slate-800 bg-slate-900 shadow-2xl',
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-slate-800 px-5 py-3">
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-slate-100">{{ title }}</h2>
292
- <p v-if="subtitle" class="truncate text-[11px] text-slate-400">{{ subtitle }}</p>
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-slate-400 hover:bg-slate-800 hover:text-slate-200"
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-slate-800 bg-slate-900/60"
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-slate-800/40"
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-slate-400" />
332
- <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
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-slate-400">
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-slate-500"
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-slate-800 bg-slate-900/60"
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-slate-800/40"
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-emerald-400' : 'text-rose-400'"
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-slate-400">
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-emerald-500/15 text-emerald-300'
382
- : 'bg-rose-500/15 text-rose-300'
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-slate-400">
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-slate-500"
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-slate-800 bg-slate-900/60"
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-slate-800/40"
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-emerald-400' : 'text-amber-400'"
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-slate-400">
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-emerald-500/15 text-emerald-300'
438
- : 'bg-amber-500/15 text-amber-300'
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-slate-400">
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-slate-500"
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-slate-800 bg-slate-900/60"
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-slate-800/40"
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-slate-400" />
474
- <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
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-slate-400">
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-slate-500"
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-slate-800 px-4 py-2 text-[11px]"
29
- :class="error ? 'text-rose-300' : 'text-slate-400'"
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-sky-900/50 bg-sky-950/30 text-sky-300',
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-emerald-900/50 bg-emerald-950/30 text-emerald-300',
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-rose-900/50 bg-rose-950/30 text-rose-300',
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-slate-800 bg-slate-900/40 text-slate-400',
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-slate-500">·</span>
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-slate-500">
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-slate-300" :title="step.container.id">
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-slate-500">
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-slate-300">
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
+ }