@cat-factory/app 0.311.1 → 0.311.3

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 (195) hide show
  1. package/README.md +70 -5
  2. package/app/app.config.ts +6 -18
  3. package/app/components/assistant/AssistantModal.vue +2 -5
  4. package/app/components/auth/AuthGate.vue +2 -1
  5. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +14 -15
  6. package/app/components/binaryOutput/StoredAssetView.vue +1 -3
  7. package/app/components/board/AddTaskModal.vue +3 -10
  8. package/app/components/board/AgentFailureCard.vue +4 -5
  9. package/app/components/board/AgentStopButton.vue +5 -7
  10. package/app/components/board/BoardCanvas.vue +31 -30
  11. package/app/components/board/CreateInitiativeModal.vue +2 -4
  12. package/app/components/board/RecurringPipelineModal.vue +2 -7
  13. package/app/components/board/nodes/AgentChip.vue +3 -2
  14. package/app/components/board/nodes/BlockNode.vue +14 -11
  15. package/app/components/board/nodes/InitiativeCard.vue +6 -3
  16. package/app/components/bootstrap/AdoptionReviewModal.vue +4 -7
  17. package/app/components/bootstrap/BootstrapModal.vue +6 -15
  18. package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
  19. package/app/components/brainstorm/BrainstormWindow.vue +32 -26
  20. package/app/components/bugFishing/BugFishingWindow.vue +15 -13
  21. package/app/components/clarity/ClarityReviewWindow.vue +31 -26
  22. package/app/components/common/AsyncViewError.vue +1 -1
  23. package/app/components/common/ClarificationItem.vue +3 -1
  24. package/app/components/common/EmptyState.vue +41 -19
  25. package/app/components/common/InterviewGateNotice.vue +3 -1
  26. package/app/components/common/Spinner.vue +28 -0
  27. package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
  28. package/app/components/docs/DocInterviewWindow.vue +6 -6
  29. package/app/components/documents/ContextDocumentPicker.vue +2 -2
  30. package/app/components/documents/DocumentImportModal.vue +9 -8
  31. package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
  32. package/app/components/documents/DocumentTemplatesModal.vue +15 -9
  33. package/app/components/documents/RepoContextDocPicker.vue +2 -2
  34. package/app/components/documents/SpawnPreviewModal.vue +4 -6
  35. package/app/components/documents/StartFromDesignModal.vue +2 -1
  36. package/app/components/documents/TaskContextDocs.vue +4 -5
  37. package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
  38. package/app/components/environments/steps/EnvPickStep.vue +7 -3
  39. package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
  40. package/app/components/focus/BlockFocusView.vue +6 -5
  41. package/app/components/followUp/FollowUpWindow.vue +7 -9
  42. package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
  43. package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
  44. package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
  45. package/app/components/foundational/FoundationalServiceSources.vue +20 -22
  46. package/app/components/foundational/FoundationalSuppressions.vue +2 -2
  47. package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
  48. package/app/components/fragments/FragmentLibraryManager.vue +36 -34
  49. package/app/components/fragments/FragmentSelector.vue +7 -3
  50. package/app/components/gates/GateResultView.vue +6 -9
  51. package/app/components/github/AddServiceFromRepoModal.vue +6 -10
  52. package/app/components/github/GitHubConnect.vue +10 -17
  53. package/app/components/github/GitHubPanel.vue +44 -47
  54. package/app/components/github/RepoTreeBrowser.vue +9 -5
  55. package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
  56. package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
  57. package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
  58. package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
  59. package/app/components/humanTest/HumanTestWindow.vue +2 -1
  60. package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
  61. package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
  62. package/app/components/judge/JudgeResultView.vue +2 -7
  63. package/app/components/kaizen/KaizenPanel.vue +20 -5
  64. package/app/components/kaizen/KaizenStepStatus.vue +2 -1
  65. package/app/components/layout/AccountAuditLog.vue +11 -3
  66. package/app/components/layout/AccountDeploymentSettings.vue +17 -62
  67. package/app/components/layout/AccountFailureKindRules.vue +6 -5
  68. package/app/components/layout/AccountModelPolicySettings.vue +2 -6
  69. package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
  70. package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
  71. package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
  72. package/app/components/layout/AccountTeamSettings.vue +12 -9
  73. package/app/components/layout/BoardSwitcher.vue +2 -2
  74. package/app/components/layout/BoardToolbar.vue +3 -11
  75. package/app/components/layout/CommandBar.vue +7 -3
  76. package/app/components/layout/ConnectionStatusBanner.vue +2 -1
  77. package/app/components/layout/IntegrationsHub.vue +7 -3
  78. package/app/components/layout/ModelProvidersHub.vue +7 -3
  79. package/app/components/layout/NotificationsInbox.vue +3 -11
  80. package/app/components/layout/SideBar.vue +1 -1
  81. package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
  82. package/app/components/media/ArtifactLightbox.vue +4 -3
  83. package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
  84. package/app/components/observability/ToolCallList.vue +12 -17
  85. package/app/components/panels/AgentStepDetail.vue +5 -5
  86. package/app/components/panels/GenericStructuredResultView.vue +6 -9
  87. package/app/components/panels/InspectorPanel.vue +2 -1
  88. package/app/components/panels/MergerResultView.vue +5 -6
  89. package/app/components/panels/ObservabilityPanel.vue +26 -41
  90. package/app/components/panels/OperatorDashboardPanel.vue +29 -17
  91. package/app/components/panels/ReportsPanel.vue +17 -11
  92. package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
  93. package/app/components/panels/ResultWindowShell.vue +1 -1
  94. package/app/components/panels/RunDetailLoadState.vue +4 -3
  95. package/app/components/panels/StepContainerStatus.vue +4 -3
  96. package/app/components/panels/StepDelegatedStatus.vue +3 -2
  97. package/app/components/panels/StepMetadataCard.vue +6 -8
  98. package/app/components/panels/StepRunMeta.vue +2 -5
  99. package/app/components/panels/inspector/ContainerSummary.vue +7 -1
  100. package/app/components/panels/inspector/EpicChildren.vue +7 -3
  101. package/app/components/panels/inspector/FrontendConfig.vue +7 -3
  102. package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
  103. package/app/components/panels/inspector/ServiceConnections.vue +7 -3
  104. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
  105. package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
  106. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
  107. package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
  108. package/app/components/panels/inspector/TaskExecution.vue +5 -6
  109. package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
  110. package/app/components/panels/inspector/TaskStructure.vue +0 -1
  111. package/app/components/pipeline/AgentPromptEditor.vue +25 -18
  112. package/app/components/pipeline/PipelineBuilder.vue +14 -24
  113. package/app/components/pipeline/PipelineProgress.vue +16 -17
  114. package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
  115. package/app/components/prReview/PrReviewWindow.vue +16 -20
  116. package/app/components/providers/ApiKeysSection.vue +2 -6
  117. package/app/components/providers/PersonalCredentialModal.vue +2 -6
  118. package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
  119. package/app/components/providers/VendorCredentialsModal.vue +2 -6
  120. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
  121. package/app/components/ralph/RalphLoopResultView.vue +2 -7
  122. package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
  123. package/app/components/sandbox/SandboxPanel.vue +37 -25
  124. package/app/components/settings/AccountSettingsPanel.vue +6 -3
  125. package/app/components/settings/ApiTokensPanel.vue +2 -6
  126. package/app/components/settings/BudgetSettings.vue +2 -2
  127. package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
  128. package/app/components/settings/CloudflareHandlerSection.vue +3 -7
  129. package/app/components/settings/ConsensusGroupsSection.vue +7 -3
  130. package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
  131. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
  132. package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
  133. package/app/components/settings/IssueTrackerPanel.vue +9 -9
  134. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  135. package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
  136. package/app/components/settings/McpAuthorizeScreen.vue +8 -3
  137. package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
  138. package/app/components/settings/ModelConfigurationPanel.vue +45 -17
  139. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
  140. package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
  141. package/app/components/settings/PackageRegistriesPanel.vue +2 -6
  142. package/app/components/settings/ProviderConnectionTab.vue +3 -7
  143. package/app/components/settings/RiskPolicyPanel.vue +4 -16
  144. package/app/components/settings/SharedStacksPanel.vue +8 -18
  145. package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
  146. package/app/components/settings/UsageSettings.vue +11 -2
  147. package/app/components/settings/UserSecretsSection.vue +5 -7
  148. package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
  149. package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
  150. package/app/components/skills/ReviewSkillQueue.vue +7 -3
  151. package/app/components/skills/SkillLibraryManager.vue +20 -18
  152. package/app/components/slack/SlackPanel.vue +5 -8
  153. package/app/components/spec/ServiceSpecWindow.vue +32 -29
  154. package/app/components/tasks/BugHuntModal.vue +23 -19
  155. package/app/components/tasks/ContextIssuePicker.vue +2 -2
  156. package/app/components/tasks/TaskContextIssues.vue +4 -5
  157. package/app/components/tasks/TaskImportModal.vue +10 -8
  158. package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
  159. package/app/components/testing/TestReportWindow.vue +6 -12
  160. package/app/components/tutorial/TutorialCatalogue.vue +7 -3
  161. package/app/components/tutorial/TutorialOverlay.vue +2 -1
  162. package/app/components/tutorial/TutorialPrompt.vue +7 -3
  163. package/app/components/vcs/GitLabConnect.vue +2 -6
  164. package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
  165. package/app/composables/toastTitleKeys.spec.ts +143 -0
  166. package/app/composables/useActionToast.spec.ts +71 -0
  167. package/app/composables/useActionToast.ts +101 -0
  168. package/app/composables/useBlockDeletion.ts +3 -6
  169. package/app/composables/useConfirmAction.ts +5 -3
  170. package/app/composables/useContextLinking.spec.ts +3 -0
  171. package/app/composables/useContextLinking.ts +11 -16
  172. package/app/composables/useCopyToClipboard.ts +4 -3
  173. package/app/composables/useNavContributions.ts +4 -5
  174. package/app/composables/usePipelineErrorToast.spec.ts +24 -0
  175. package/app/composables/usePipelineErrorToast.ts +26 -2
  176. package/app/composables/usePipelineLibraryActions.ts +8 -9
  177. package/app/pages/index.vue +3 -2
  178. package/app/stores/board/context.ts +4 -3
  179. package/app/stores/board/placement.ts +8 -20
  180. package/app/stores/board/removal.ts +4 -13
  181. package/app/stores/board.spec.ts +10 -1
  182. package/app/stores/board.ts +3 -2
  183. package/app/utils/bootstrapSteps.ts +11 -2
  184. package/app/utils/pipelineRender.ts +21 -8
  185. package/i18n/locales/de.json +12 -30
  186. package/i18n/locales/en.json +12 -30
  187. package/i18n/locales/es.json +12 -30
  188. package/i18n/locales/fr.json +12 -30
  189. package/i18n/locales/he.json +12 -30
  190. package/i18n/locales/it.json +12 -30
  191. package/i18n/locales/ja.json +12 -30
  192. package/i18n/locales/pl.json +12 -30
  193. package/i18n/locales/tr.json +12 -30
  194. package/i18n/locales/uk.json +12 -30
  195. package/package.json +1 -1
@@ -32,6 +32,7 @@ import RunFailureSummary from '~/components/observability/RunFailureSummary.vue'
32
32
  import ToolCallList from '~/components/observability/ToolCallList.vue'
33
33
  import SectionLabel from '~/components/common/SectionLabel.vue'
34
34
  import IconButton from '~/components/common/IconButton.vue'
35
+ import EmptyState from '~/components/common/EmptyState.vue'
35
36
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
36
37
 
37
38
  /** No run selected: the same empty, NOT-truncated trajectory the store answers with. */
@@ -844,13 +845,9 @@ function exportJson() {
844
845
  </section>
845
846
 
846
847
  <!-- states -->
847
- <p
848
- v-if="loading && !calls.length"
849
- class="flex items-center gap-2 py-8 text-center text-sm text-dimmed justify-center"
850
- >
851
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
852
- {{ t('observability.loadingActivity') }}
853
- </p>
848
+ <div v-if="loading && !calls.length" class="space-y-2">
849
+ <USkeleton v-for="n in 3" :key="n" class="h-10 w-full rounded-xl" />
850
+ </div>
854
851
  <div
855
852
  v-else-if="error"
856
853
  class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-app-error-900/60 py-6 text-center text-sm text-app-error-400"
@@ -867,12 +864,11 @@ function exportJson() {
867
864
  {{ t('common.retry') }}
868
865
  </UButton>
869
866
  </div>
870
- <p
867
+ <EmptyState
871
868
  v-else-if="!calls.length"
872
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
873
- >
874
- {{ t('observability.noCalls') }}
875
- </p>
869
+ icon="i-lucide-activity"
870
+ :title="t('observability.noCalls')"
871
+ />
876
872
 
877
873
  <!-- per-call list, narrowable by outcome -->
878
874
  <template v-else>
@@ -886,12 +882,11 @@ function exportJson() {
886
882
  <!-- Narrowed to nothing reads differently from recorded nothing, and on this
887
883
  surface it is the good news: the operator asked for the failures and there
888
884
  are none. -->
889
- <p
885
+ <EmptyState
890
886
  v-if="!visibleCalls.length"
891
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
892
- >
893
- {{ t('observability.noCallsMatching') }}
894
- </p>
887
+ icon="i-lucide-list-filter"
888
+ :title="t('observability.noCallsMatching')"
889
+ />
895
890
 
896
891
  <ul v-else class="space-y-2">
897
892
  <li
@@ -1092,13 +1087,9 @@ function exportJson() {
1092
1087
 
1093
1088
  <!-- Provided context: the complete context each container agent was given. -->
1094
1089
  <div v-else-if="view === 'context'" class="mx-auto max-w-4xl space-y-5">
1095
- <p
1096
- v-if="contextLoading && !contextSnapshots.length"
1097
- class="flex items-center justify-center gap-2 py-8 text-center text-sm text-dimmed"
1098
- >
1099
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
1100
- {{ t('observability.loadingContext') }}
1101
- </p>
1090
+ <div v-if="contextLoading && !contextSnapshots.length" class="space-y-2">
1091
+ <USkeleton v-for="n in 3" :key="n" class="h-10 w-full rounded-xl" />
1092
+ </div>
1102
1093
  <div
1103
1094
  v-else-if="contextError && !contextSnapshots.length"
1104
1095
  class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-app-error-900/60 py-8 text-center text-sm text-app-error-400"
@@ -1115,12 +1106,11 @@ function exportJson() {
1115
1106
  {{ t('common.retry') }}
1116
1107
  </UButton>
1117
1108
  </div>
1118
- <p
1109
+ <EmptyState
1119
1110
  v-else-if="!contextSnapshots.length"
1120
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
1121
- >
1122
- {{ t('observability.noContext') }}
1123
- </p>
1111
+ icon="i-lucide-file-text"
1112
+ :title="t('observability.noContext')"
1113
+ />
1124
1114
 
1125
1115
  <ul v-else class="space-y-2">
1126
1116
  <li
@@ -1251,19 +1241,14 @@ function exportJson() {
1251
1241
  </span>
1252
1242
  </section>
1253
1243
 
1254
- <p
1255
- v-if="searchLoading && !searchQueries.length"
1256
- class="flex items-center justify-center gap-2 py-8 text-center text-sm text-dimmed"
1257
- >
1258
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
1259
- {{ t('observability.loadingSearch') }}
1260
- </p>
1261
- <p
1244
+ <div v-if="searchLoading && !searchQueries.length" class="space-y-2">
1245
+ <USkeleton v-for="n in 3" :key="n" class="h-10 w-full rounded-xl" />
1246
+ </div>
1247
+ <EmptyState
1262
1248
  v-else-if="!searchQueries.length"
1263
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
1264
- >
1265
- {{ t('observability.noSearch') }}
1266
- </p>
1249
+ icon="i-lucide-search"
1250
+ :title="t('observability.noSearch')"
1251
+ />
1267
1252
 
1268
1253
  <div v-else>
1269
1254
  <SectionLabel class="mb-2">
@@ -7,6 +7,7 @@ import { formatMs } from '~/utils/observability'
7
7
  import { FAILURE_KIND_KEYS, isAgentFailureKind } from '~/utils/failureKinds'
8
8
  import SectionLabel from '~/components/common/SectionLabel.vue'
9
9
  import IconButton from '~/components/common/IconButton.vue'
10
+ import EmptyState from '~/components/common/EmptyState.vue'
10
11
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
11
12
 
12
13
  // Deployment-level (platform-operator) observability dashboard: the aggregate health of the
@@ -166,10 +167,11 @@ watch(
166
167
  color="neutral"
167
168
  variant="ghost"
168
169
  icon="i-lucide-refresh-cw"
170
+ :loading="loading"
169
171
  :label="t('platformObservability.refresh')"
170
172
  :ui="{
171
173
  base: 'rounded-lg border border-default p-1.5 text-muted transition hover:text-default',
172
- leadingIcon: loading ? 'h-4 w-4 animate-spin' : 'h-4 w-4',
174
+ leadingIcon: 'h-4 w-4',
173
175
  }"
174
176
  data-testid="operator-refresh"
175
177
  @click="refresh"
@@ -205,8 +207,9 @@ watch(
205
207
  </UButton>
206
208
  </div>
207
209
 
208
- <div v-else-if="loading && !view" class="py-16 text-center text-sm text-muted">
209
- {{ t('platformObservability.loading') }}
210
+ <div v-else-if="loading && !view" class="mx-auto flex max-w-5xl flex-col gap-6">
211
+ <USkeleton class="h-20 w-full rounded-lg" />
212
+ <USkeleton v-for="n in 2" :key="n" class="h-36 w-full rounded-lg" />
210
213
  </div>
211
214
 
212
215
  <div v-else-if="view" class="mx-auto flex max-w-5xl flex-col gap-6">
@@ -299,9 +302,12 @@ watch(
299
302
  {{ t('platformObservability.trend.title') }}
300
303
  </SectionLabel>
301
304
  <div class="rounded-lg border border-default bg-default/40 p-4">
302
- <div v-if="view.outcomes.total === 0" class="py-6 text-center text-xs text-dimmed">
303
- {{ t('platformObservability.trend.empty') }}
304
- </div>
305
+ <EmptyState
306
+ v-if="view.outcomes.total === 0"
307
+ compact
308
+ icon="i-lucide-chart-column"
309
+ :title="t('platformObservability.trend.empty')"
310
+ />
305
311
  <div v-else class="flex h-28 items-end gap-0.5" data-testid="operator-trend">
306
312
  <div
307
313
  v-for="p in view.trend.points"
@@ -349,9 +355,12 @@ watch(
349
355
  {{ t('platformObservability.gates.title') }}
350
356
  </SectionLabel>
351
357
  <div class="overflow-x-auto rounded-lg border border-default bg-default/40 p-4">
352
- <p v-if="!view.gates.length" class="py-4 text-center text-xs text-dimmed">
353
- {{ t('platformObservability.gates.empty') }}
354
- </p>
358
+ <EmptyState
359
+ v-if="!view.gates.length"
360
+ compact
361
+ icon="i-lucide-shield-check"
362
+ :title="t('platformObservability.gates.empty')"
363
+ />
355
364
  <UTable
356
365
  v-else
357
366
  :data="view.gates"
@@ -412,9 +421,12 @@ watch(
412
421
  {{ t('platformObservability.failures.title') }}
413
422
  </SectionLabel>
414
423
  <div class="rounded-lg border border-default bg-default/40 p-4">
415
- <div v-if="!view.failures.length" class="py-4 text-center text-xs text-dimmed">
416
- {{ t('platformObservability.failures.empty') }}
417
- </div>
424
+ <EmptyState
425
+ v-if="!view.failures.length"
426
+ compact
427
+ icon="i-lucide-circle-x"
428
+ :title="t('platformObservability.failures.empty')"
429
+ />
418
430
  <ul v-else class="flex flex-col gap-2" data-testid="operator-failures">
419
431
  <li v-for="f in view.failures" :key="f.kind" class="text-xs">
420
432
  <div class="mb-0.5 flex items-center justify-between">
@@ -475,12 +487,12 @@ watch(
475
487
  {{ t('platformObservability.durations.title') }}
476
488
  </SectionLabel>
477
489
  <div class="rounded-lg border border-default bg-default/40 p-3 text-sm">
478
- <div
490
+ <EmptyState
479
491
  v-if="view.durations.count === 0"
480
- class="py-2 text-center text-xs text-dimmed"
481
- >
482
- {{ t('platformObservability.durations.empty') }}
483
- </div>
492
+ compact
493
+ icon="i-lucide-timer"
494
+ :title="t('platformObservability.durations.empty')"
495
+ />
484
496
  <dl
485
497
  v-else
486
498
  class="grid grid-cols-3 gap-y-3 text-center"
@@ -21,6 +21,7 @@ import {
21
21
  import ReportsSpendBreakdown from '~/components/panels/ReportsSpendBreakdown.vue'
22
22
  import SectionLabel from '~/components/common/SectionLabel.vue'
23
23
  import IconButton from '~/components/common/IconButton.vue'
24
+ import EmptyState from '~/components/common/EmptyState.vue'
24
25
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
25
26
 
26
27
  // Reports: cross-cutting usage analytics for the active account — where the spend and the
@@ -245,10 +246,11 @@ watch(
245
246
  color="neutral"
246
247
  variant="ghost"
247
248
  icon="i-lucide-refresh-cw"
249
+ :loading="loading"
248
250
  :label="t('reports.refresh')"
249
251
  :ui="{
250
252
  base: 'rounded-lg border border-default p-1.5 text-muted transition hover:text-default',
251
- leadingIcon: loading ? 'h-4 w-4 animate-spin' : 'h-4 w-4',
253
+ leadingIcon: 'h-4 w-4',
252
254
  }"
253
255
  @click="refresh"
254
256
  />
@@ -283,8 +285,9 @@ watch(
283
285
  </UButton>
284
286
  </div>
285
287
 
286
- <div v-else-if="loading && !view" class="py-16 text-center text-sm text-muted">
287
- {{ t('reports.loading') }}
288
+ <div v-else-if="loading && !view" class="mx-auto flex max-w-5xl flex-col gap-6">
289
+ <USkeleton class="h-20 w-full rounded-lg" />
290
+ <USkeleton v-for="n in 2" :key="n" class="h-36 w-full rounded-lg" />
288
291
  </div>
289
292
 
290
293
  <div v-else-if="view" class="mx-auto flex max-w-5xl flex-col gap-6">
@@ -374,9 +377,12 @@ watch(
374
377
  {{ t('reports.trend.title') }}
375
378
  </SectionLabel>
376
379
  <div class="rounded-lg border border-default bg-default/40 p-4">
377
- <div v-if="!hasSpend" class="py-6 text-center text-xs text-dimmed">
378
- {{ t('reports.trend.empty') }}
379
- </div>
380
+ <EmptyState
381
+ v-if="!hasSpend"
382
+ compact
383
+ icon="i-lucide-chart-column"
384
+ :title="t('reports.trend.empty')"
385
+ />
380
386
  <div v-else class="flex h-28 items-end gap-0.5" data-testid="reports-trend">
381
387
  <div
382
388
  v-for="point in view.trend.points"
@@ -505,12 +511,12 @@ watch(
505
511
  {{ t('reports.activity.heading') }}
506
512
  </h3>
507
513
  <div class="rounded-lg border border-default bg-default/40 p-4">
508
- <div
514
+ <EmptyState
509
515
  v-if="!activityByDimension.length"
510
- class="py-4 text-center text-xs text-dimmed"
511
- >
512
- {{ t('reports.activity.empty') }}
513
- </div>
516
+ compact
517
+ icon="i-lucide-activity"
518
+ :title="t('reports.activity.empty')"
519
+ />
514
520
  <template v-else>
515
521
  <ul class="flex flex-col gap-3" data-testid="reports-activity">
516
522
  <li
@@ -2,6 +2,7 @@
2
2
  import { computed } from 'vue'
3
3
  import type { ReportSpendCap, ReportSpendRow } from '~/types/execution'
4
4
  import { maxOf, segmentPct, spendMagnitude } from './ReportsPanel.logic'
5
+ import EmptyState from '~/components/common/EmptyState.vue'
5
6
 
6
7
  // One ranked spend breakdown: a horizontal bar per slice, split into the metered
7
8
  // (`violet-500`, real money) and subscription (`amber-600`, illustrative equivalent-API
@@ -34,9 +35,12 @@ const max = computed(() => maxOf(props.rows, spendMagnitude))
34
35
 
35
36
  <template>
36
37
  <div class="rounded-lg border border-default bg-default/40 p-4">
37
- <div v-if="!rows.length" class="py-4 text-center text-xs text-dimmed">
38
- {{ t('reports.spend.empty') }}
39
- </div>
38
+ <EmptyState
39
+ v-if="!rows.length"
40
+ compact
41
+ icon="i-lucide-wallet"
42
+ :title="t('reports.spend.empty')"
43
+ />
40
44
  <ul v-else class="flex flex-col gap-2.5" :data-testid="testId">
41
45
  <li v-for="row in rows" :key="row.key" class="text-xs" data-testid="reports-spend-row">
42
46
  <div class="mb-1 flex items-baseline justify-between gap-2">
@@ -257,7 +257,7 @@ const chipClass = computed(() =>
257
257
 
258
258
  const backdropClass = computed(() => [
259
259
  WINDOWED_OVERLAY_CLASS,
260
- 'z-50 justify-center bg-app-950/70 backdrop-blur-sm',
260
+ 'z-50 justify-center bg-elevated/75 backdrop-blur-sm',
261
261
  props.variant === 'centered' ? 'items-center' : 'items-stretch',
262
262
  ])
263
263
  // The centered cap is the smaller of 90dvh and the backdrop's padded box: with a large inset (a
@@ -12,6 +12,7 @@
12
12
  * event delivered and every run already fetched once.
13
13
  */
14
14
  import { computed } from 'vue'
15
+ import Spinner from '~/components/common/Spinner.vue'
15
16
 
16
17
  const props = defineProps<{ instanceId: string | null }>()
17
18
 
@@ -29,10 +30,10 @@ const error = computed(() => execution.fullError(props.instanceId))
29
30
  :class="error ? 'text-app-error-300' : 'text-muted'"
30
31
  data-testid="run-detail-load-state"
31
32
  >
32
- <UIcon
33
- :name="error ? 'i-lucide-triangle-alert' : 'i-lucide-loader-circle'"
33
+ <Spinner
34
+ :name="error ? 'i-lucide-triangle-alert' : undefined"
35
+ :spinning="!error"
34
36
  class="h-3.5 w-3.5 shrink-0"
35
- :class="error ? '' : 'animate-spin'"
36
37
  />
37
38
  <span>{{
38
39
  error ? t('panels.runDetail.loadFailed', { reason: error }) : t('panels.runDetail.loading')
@@ -4,6 +4,7 @@ import type { PipelineStep, RunContainerStatus } from '~/types/execution'
4
4
  import { containerPhaseLabel } from '~/utils/pipelineRender'
5
5
  import SectionLabel from '~/components/common/SectionLabel.vue'
6
6
  import IconButton from '~/components/common/IconButton.vue'
7
+ import Spinner from '~/components/common/Spinner.vue'
7
8
 
8
9
  // The per-run container lifecycle for a container-backed step: its status (spinning up /
9
10
  // running / errored / reclaimed), the live phase (preparing the checkout vs the agent
@@ -74,7 +75,7 @@ const CONTAINER_STATUS_META: Record<
74
75
 
75
76
  // Whether the status icon animates: only a cold-boot genuinely in flight, i.e. one whose run is
76
77
  // still being driven. A `starting` container on a parked or terminated run keeps its label and
77
- // freezes, the same way a mid-flight subtask item does (`subtaskIconClass`).
78
+ // freezes, the same way a mid-flight subtask item does (`subtaskIconSpins`).
78
79
  const spinIcon = computed(
79
80
  () =>
80
81
  props.runActive && !!containerStatus.value && CONTAINER_STATUS_META[containerStatus.value].spin,
@@ -100,10 +101,10 @@ const { copy: copyText } = useCopyToClipboard()
100
101
  :class="CONTAINER_STATUS_META[containerStatus].cls"
101
102
  >
102
103
  <div class="flex items-center gap-2">
103
- <UIcon
104
+ <Spinner
104
105
  :name="CONTAINER_STATUS_META[containerStatus].icon"
106
+ :spinning="spinIcon"
105
107
  class="h-4 w-4 shrink-0"
106
- :class="spinIcon ? 'animate-spin' : ''"
107
108
  />
108
109
  <span class="font-medium">{{ t(CONTAINER_STATUS_KEYS[containerStatus]) }}</span>
109
110
  <template v-if="phaseLabel && containerStatus === 'up'">
@@ -8,6 +8,7 @@ import {
8
8
  import type { PipelineStep } from '~/types/execution'
9
9
  import SectionLabel from '~/components/common/SectionLabel.vue'
10
10
  import IconButton from '~/components/common/IconButton.vue'
11
+ import Spinner from '~/components/common/Spinner.vue'
11
12
 
12
13
  // The EXTERNAL work a delegated step dispatched: which registered executor is running it, what it
13
14
  // is doing, and the link to that system's own logs, which is the affordance everything else here
@@ -81,10 +82,10 @@ const { copy: copyText } = useCopyToClipboard()
81
82
  <div v-if="record" data-testid="step-delegated-status">
82
83
  <div class="rounded-lg border px-3 py-2 text-xs" :class="statusView.meta.cls">
83
84
  <div class="flex items-center gap-2">
84
- <UIcon
85
+ <Spinner
85
86
  :name="statusView.meta.icon"
87
+ :spinning="statusView.meta.spin"
86
88
  class="h-4 w-4 shrink-0"
87
- :class="statusView.meta.spin ? 'animate-spin' : ''"
88
89
  />
89
90
  <!-- A status this build does not know is NAMED as unrecognised, carrying the stored value,
90
91
  rather than guessed onto a current one or silently dropped. -->
@@ -7,13 +7,14 @@ import {
7
7
  } from '@cat-factory/contracts'
8
8
  import type { AgentState, PipelineStep, CompanionVerdict, StepApproval } from '~/types/execution'
9
9
  import type { BadgeColor } from '~/utils/badge'
10
- import { subtaskIconClass } from '~/utils/pipelineRender'
10
+ import { subtaskIconSpins, subtaskIconTone } from '~/utils/pipelineRender'
11
11
  import StepModelActivity from '~/components/observability/StepModelActivity.vue'
12
12
  import StepContainerStatus from '~/components/panels/StepContainerStatus.vue'
13
13
  import StepDelegatedStatus from '~/components/panels/StepDelegatedStatus.vue'
14
14
  import CopyButton from '~/components/common/CopyButton.vue'
15
15
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
16
16
  import SectionLabel from '~/components/common/SectionLabel.vue'
17
+ import Spinner from '~/components/common/Spinner.vue'
17
18
 
18
19
  // The step's metadata card body: state/timing/model/run id, the container cold-boot
19
20
  // phase, the live subtask breakdown, the LLM observability rollup, the applied
@@ -204,11 +205,7 @@ async function copyRunId() {
204
205
  {{ t('panels.stepMeta.duration') }}
205
206
  </SectionLabel>
206
207
  <dd class="mt-0.5 flex items-center gap-1.5 tabular-nums text-default">
207
- <UIcon
208
- v-if="isRunning"
209
- name="i-lucide-loader-circle"
210
- class="h-3 w-3 animate-spin text-primary"
211
- />
208
+ <Spinner v-if="isRunning" class="h-3 w-3 text-primary" />
212
209
  <span v-if="durationLabel">{{ durationLabel }}</span>
213
210
  <span v-else class="text-dimmed">—</span>
214
211
  <span v-if="isRunning" class="text-2xs text-dimmed">{{
@@ -314,10 +311,11 @@ async function copyRunId() {
314
311
  : 'text-muted'
315
312
  "
316
313
  >
317
- <UIcon
314
+ <Spinner
318
315
  :name="ITEM_ICON[item.status]"
316
+ :spinning="subtaskIconSpins(item.status, runFailed)"
319
317
  class="mt-px h-3 w-3 shrink-0"
320
- :class="subtaskIconClass(item.status, runFailed)"
318
+ :class="subtaskIconTone(item.status)"
321
319
  />
322
320
  <span>{{ item.label }}</span>
323
321
  </li>
@@ -4,6 +4,7 @@ import type { PipelineStep } from '~/types/execution'
4
4
  import { useStepTimer } from '~/composables/useStepTimer'
5
5
  import StepModelActivity from '~/components/observability/StepModelActivity.vue'
6
6
  import SectionLabel from '~/components/common/SectionLabel.vue'
7
+ import Spinner from '~/components/common/Spinner.vue'
7
8
 
8
9
  // Shared run-metadata + observability block for the step-backed result windows
9
10
  // (the CI/conflicts gate, the tester report). It carries the facts every step has in
@@ -71,11 +72,7 @@ async function copyRunId() {
71
72
  {{ t('panels.stepMeta.duration') }}
72
73
  </SectionLabel>
73
74
  <p class="flex items-center gap-1.5 text-xs tabular-nums text-toned">
74
- <UIcon
75
- v-if="isRunning"
76
- name="i-lucide-loader-circle"
77
- class="h-3 w-3 animate-spin text-primary"
78
- />
75
+ <Spinner v-if="isRunning" class="h-3 w-3 text-primary" />
79
76
  {{ durationLabel }}
80
77
  <span v-if="isRunning" class="text-2xs text-dimmed">{{
81
78
  t('panels.stepMeta.elapsed')
@@ -3,6 +3,7 @@ import type { Block } from '~/types/domain'
3
3
  import { STATUS_META } from '~/utils/catalog'
4
4
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
5
5
  import ReviewDebtBadge from '~/components/board/ReviewDebtBadge.vue'
6
+ import EmptyState from '~/components/common/EmptyState.vue'
6
7
 
7
8
  const props = defineProps<{ block: Block }>()
8
9
 
@@ -79,7 +80,12 @@ function addTask() {
79
80
  <span class="ms-auto text-3xs text-dimmed">{{ STATUS_META[task.status].label }}</span>
80
81
  </li>
81
82
  </ul>
82
- <div v-else class="text-2xs text-dimmed">{{ t('inspector.container.noTasks') }}</div>
83
+ <EmptyState
84
+ v-else
85
+ compact
86
+ icon="i-lucide-list-todo"
87
+ :title="t('inspector.container.noTasks')"
88
+ />
83
89
  <p v-if="isFrame" class="text-2xs text-dimmed">
84
90
  {{ t('inspector.container.servicesHint') }}
85
91
  </p>
@@ -4,6 +4,7 @@ import type { Block } from '~/types/domain'
4
4
  import { STATUS_META } from '~/utils/catalog'
5
5
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
6
6
  import SectionLabel from '~/components/common/SectionLabel.vue'
7
+ import EmptyState from '~/components/common/EmptyState.vue'
7
8
 
8
9
  // The epic inspector body: the full tree of member tasks (which may live under different
9
10
  // services/modules), grouped service → module → task. Each task row selects it. Membership
@@ -58,9 +59,12 @@ const groups = computed(() => {
58
59
  }}</span>
59
60
  </template>
60
61
 
61
- <div v-if="members.length === 0" class="text-2xs text-dimmed">
62
- {{ t('inspector.epicChildren.empty') }}
63
- </div>
62
+ <EmptyState
63
+ v-if="members.length === 0"
64
+ compact
65
+ icon="i-lucide-list-tree"
66
+ :title="t('inspector.epicChildren.empty')"
67
+ />
64
68
 
65
69
  <div v-else class="space-y-2">
66
70
  <div v-for="(group, gi) in groups" :key="gi" class="rounded-md border border-muted/60 p-2">
@@ -14,6 +14,7 @@ import FrontendBindingsResolved from '~/components/panels/inspector/FrontendBind
14
14
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
15
15
  import IconButton from '~/components/common/IconButton.vue'
16
16
  import { apiErrorEnvelope } from '~/composables/api/errors'
17
+ import EmptyState from '~/components/common/EmptyState.vue'
17
18
 
18
19
  // Frontend-frame (`type: 'frontend'`) configuration: how to build, serve, and mock this
19
20
  // frontend for a self-contained UI test (+ an optional browsable preview on local/node),
@@ -565,9 +566,12 @@ onUnmounted(() => preview.stopPolling(props.block.id))
565
566
  />
566
567
  </div>
567
568
  </div>
568
- <div v-else class="text-2xs text-dimmed">
569
- {{ t('inspector.frontendConfig.bindings.empty') }}
570
- </div>
569
+ <EmptyState
570
+ v-else
571
+ compact
572
+ icon="i-lucide-link"
573
+ :title="t('inspector.frontendConfig.bindings.empty')"
574
+ />
571
575
 
572
576
  <!-- How the bindings resolve RIGHT NOW: each env var → a bound service's live ephemeral
573
577
  URL, or WireMock — plus the duplicate-env-var warning. The same view a UI-test run
@@ -12,6 +12,7 @@ import {
12
12
  INITIATIVE_STATUS_LABEL_KEYS,
13
13
  initiativeProgress,
14
14
  } from '~/utils/initiative'
15
+ import Spinner from '~/components/common/Spinner.vue'
15
16
 
16
17
  const props = defineProps<{ block: Block }>()
17
18
 
@@ -93,10 +94,12 @@ function control(action: 'pause' | 'resume' | 'cancel') {
93
94
  color="neutral"
94
95
  variant="soft"
95
96
  size="sm"
96
- icon="i-lucide-loader-circle"
97
- :ui="{ leadingIcon: 'animate-spin' }"
98
97
  @click="openPlanning"
99
98
  >
99
+ <!-- Not `:loading`: that disables the button, and this one has to stay clickable. -->
100
+ <template #leading>
101
+ <Spinner class="size-4" />
102
+ </template>
100
103
  {{ t('initiative.inspector.planningInProgress') }}
101
104
  </UButton>
102
105
  <UButton
@@ -3,6 +3,7 @@ import { computed } from 'vue'
3
3
  import type { Block, ServiceConnection } from '~/types/domain'
4
4
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
5
5
  import IconButton from '~/components/common/IconButton.vue'
6
+ import EmptyState from '~/components/common/EmptyState.vue'
6
7
 
7
8
  // Service-frame (`type: 'service'`) connections: the other services this one USES
8
9
  // (consumer→provider edges, stored on this frame — the consumer end). Each row picks a
@@ -136,9 +137,12 @@ const usedBy = computed(() =>
136
137
  />
137
138
  </div>
138
139
  </div>
139
- <div v-else class="text-2xs text-dimmed">
140
- {{ t('inspector.serviceConnections.empty') }}
141
- </div>
140
+ <EmptyState
141
+ v-else
142
+ compact
143
+ icon="i-lucide-plug"
144
+ :title="t('inspector.serviceConnections.empty')"
145
+ />
142
146
 
143
147
  <div v-if="usedBy.length" class="space-y-1" data-testid="service-connections-used-by">
144
148
  <span class="text-2xs text-muted">{{ t('inspector.serviceConnections.usedBy') }}</span>
@@ -13,7 +13,7 @@ const props = defineProps<{ block: Block }>()
13
13
  const store = useReleaseHealthStore()
14
14
  const ui = useUiStore()
15
15
  const uiMode = useUiModeStore()
16
- const toast = useToast()
16
+ const actionToast = useActionToast()
17
17
  const { present } = usePipelineErrorToast()
18
18
  const { t } = useI18n()
19
19
  const { confirmAction, toastDone } = useConfirmAction()
@@ -61,11 +61,7 @@ async function save() {
61
61
  sloIds: parseIds(draft.sloIds),
62
62
  envTag: draft.envTag.trim() || null,
63
63
  })
64
- toast.add({
65
- title: t('inspector.releaseHealth.savedToast'),
66
- icon: 'i-lucide-check',
67
- color: 'success',
68
- })
64
+ actionToast.success('inspector.releaseHealth.savedToast')
69
65
  } catch (e) {
70
66
  present(e, 'inspector.releaseHealth.saveFailed')
71
67
  } finally {
@@ -18,7 +18,7 @@ import { uid } from '~/utils/catalog'
18
18
  const props = defineProps<{ block: Block }>()
19
19
 
20
20
  const store = useTestSecretsStore()
21
- const toast = useToast()
21
+ const actionToast = useActionToast()
22
22
  const { present } = usePipelineErrorToast()
23
23
  const { t } = useI18n()
24
24
  const { confirmAction, toastDone } = useConfirmAction()
@@ -111,11 +111,7 @@ async function save() {
111
111
  value: r.value,
112
112
  })),
113
113
  })
114
- toast.add({
115
- title: t('inspector.testSecrets.savedToast'),
116
- icon: 'i-lucide-check',
117
- color: 'success',
118
- })
114
+ actionToast.success('inspector.testSecrets.savedToast')
119
115
  } catch (e) {
120
116
  present(e, 'inspector.testSecrets.saveFailed')
121
117
  } finally {