@cat-factory/app 0.311.2 → 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 (192) hide show
  1. package/README.md +65 -4
  2. package/app/app.config.ts +7 -0
  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 +2 -5
  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/ClarificationItem.vue +3 -1
  23. package/app/components/common/EmptyState.vue +41 -19
  24. package/app/components/common/InterviewGateNotice.vue +3 -1
  25. package/app/components/common/Spinner.vue +28 -0
  26. package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
  27. package/app/components/docs/DocInterviewWindow.vue +6 -6
  28. package/app/components/documents/ContextDocumentPicker.vue +2 -2
  29. package/app/components/documents/DocumentImportModal.vue +9 -8
  30. package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
  31. package/app/components/documents/DocumentTemplatesModal.vue +13 -7
  32. package/app/components/documents/RepoContextDocPicker.vue +2 -2
  33. package/app/components/documents/SpawnPreviewModal.vue +4 -6
  34. package/app/components/documents/StartFromDesignModal.vue +2 -1
  35. package/app/components/documents/TaskContextDocs.vue +4 -5
  36. package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
  37. package/app/components/environments/steps/EnvPickStep.vue +7 -3
  38. package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
  39. package/app/components/focus/BlockFocusView.vue +6 -5
  40. package/app/components/followUp/FollowUpWindow.vue +7 -9
  41. package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
  42. package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
  43. package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
  44. package/app/components/foundational/FoundationalServiceSources.vue +20 -22
  45. package/app/components/foundational/FoundationalSuppressions.vue +2 -2
  46. package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
  47. package/app/components/fragments/FragmentLibraryManager.vue +36 -34
  48. package/app/components/fragments/FragmentSelector.vue +7 -3
  49. package/app/components/gates/GateResultView.vue +6 -9
  50. package/app/components/github/AddServiceFromRepoModal.vue +6 -10
  51. package/app/components/github/GitHubConnect.vue +10 -17
  52. package/app/components/github/GitHubPanel.vue +44 -47
  53. package/app/components/github/RepoTreeBrowser.vue +9 -5
  54. package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
  55. package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
  56. package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
  57. package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
  58. package/app/components/humanTest/HumanTestWindow.vue +2 -1
  59. package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
  60. package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
  61. package/app/components/judge/JudgeResultView.vue +2 -7
  62. package/app/components/kaizen/KaizenPanel.vue +20 -5
  63. package/app/components/kaizen/KaizenStepStatus.vue +2 -1
  64. package/app/components/layout/AccountAuditLog.vue +11 -3
  65. package/app/components/layout/AccountDeploymentSettings.vue +17 -62
  66. package/app/components/layout/AccountFailureKindRules.vue +6 -5
  67. package/app/components/layout/AccountModelPolicySettings.vue +2 -6
  68. package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
  69. package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
  70. package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
  71. package/app/components/layout/AccountTeamSettings.vue +12 -9
  72. package/app/components/layout/BoardSwitcher.vue +2 -2
  73. package/app/components/layout/BoardToolbar.vue +3 -11
  74. package/app/components/layout/CommandBar.vue +7 -3
  75. package/app/components/layout/ConnectionStatusBanner.vue +2 -1
  76. package/app/components/layout/IntegrationsHub.vue +7 -3
  77. package/app/components/layout/ModelProvidersHub.vue +7 -3
  78. package/app/components/layout/NotificationsInbox.vue +3 -11
  79. package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
  80. package/app/components/media/ArtifactLightbox.vue +4 -3
  81. package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
  82. package/app/components/observability/ToolCallList.vue +12 -17
  83. package/app/components/panels/AgentStepDetail.vue +5 -5
  84. package/app/components/panels/GenericStructuredResultView.vue +6 -9
  85. package/app/components/panels/InspectorPanel.vue +2 -1
  86. package/app/components/panels/MergerResultView.vue +5 -6
  87. package/app/components/panels/ObservabilityPanel.vue +26 -41
  88. package/app/components/panels/OperatorDashboardPanel.vue +29 -17
  89. package/app/components/panels/ReportsPanel.vue +17 -11
  90. package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
  91. package/app/components/panels/RunDetailLoadState.vue +4 -3
  92. package/app/components/panels/StepContainerStatus.vue +4 -3
  93. package/app/components/panels/StepDelegatedStatus.vue +3 -2
  94. package/app/components/panels/StepMetadataCard.vue +6 -8
  95. package/app/components/panels/StepRunMeta.vue +2 -5
  96. package/app/components/panels/inspector/ContainerSummary.vue +7 -1
  97. package/app/components/panels/inspector/EpicChildren.vue +7 -3
  98. package/app/components/panels/inspector/FrontendConfig.vue +7 -3
  99. package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
  100. package/app/components/panels/inspector/ServiceConnections.vue +7 -3
  101. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
  102. package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
  103. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
  104. package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
  105. package/app/components/panels/inspector/TaskExecution.vue +5 -6
  106. package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
  107. package/app/components/panels/inspector/TaskStructure.vue +0 -1
  108. package/app/components/pipeline/AgentPromptEditor.vue +25 -18
  109. package/app/components/pipeline/PipelineBuilder.vue +14 -24
  110. package/app/components/pipeline/PipelineProgress.vue +16 -17
  111. package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
  112. package/app/components/prReview/PrReviewWindow.vue +16 -20
  113. package/app/components/providers/ApiKeysSection.vue +2 -6
  114. package/app/components/providers/PersonalCredentialModal.vue +2 -6
  115. package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
  116. package/app/components/providers/VendorCredentialsModal.vue +2 -6
  117. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
  118. package/app/components/ralph/RalphLoopResultView.vue +2 -7
  119. package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
  120. package/app/components/sandbox/SandboxPanel.vue +37 -25
  121. package/app/components/settings/AccountSettingsPanel.vue +6 -3
  122. package/app/components/settings/ApiTokensPanel.vue +2 -6
  123. package/app/components/settings/BudgetSettings.vue +2 -2
  124. package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
  125. package/app/components/settings/CloudflareHandlerSection.vue +3 -7
  126. package/app/components/settings/ConsensusGroupsSection.vue +7 -3
  127. package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
  128. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
  129. package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
  130. package/app/components/settings/IssueTrackerPanel.vue +9 -9
  131. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  132. package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
  133. package/app/components/settings/McpAuthorizeScreen.vue +8 -3
  134. package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
  135. package/app/components/settings/ModelConfigurationPanel.vue +45 -17
  136. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
  137. package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
  138. package/app/components/settings/PackageRegistriesPanel.vue +2 -6
  139. package/app/components/settings/ProviderConnectionTab.vue +3 -7
  140. package/app/components/settings/RiskPolicyPanel.vue +4 -16
  141. package/app/components/settings/SharedStacksPanel.vue +8 -18
  142. package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
  143. package/app/components/settings/UsageSettings.vue +11 -2
  144. package/app/components/settings/UserSecretsSection.vue +5 -7
  145. package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
  146. package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
  147. package/app/components/skills/ReviewSkillQueue.vue +7 -3
  148. package/app/components/skills/SkillLibraryManager.vue +20 -18
  149. package/app/components/slack/SlackPanel.vue +5 -8
  150. package/app/components/spec/ServiceSpecWindow.vue +32 -29
  151. package/app/components/tasks/BugHuntModal.vue +23 -19
  152. package/app/components/tasks/ContextIssuePicker.vue +2 -2
  153. package/app/components/tasks/TaskContextIssues.vue +4 -5
  154. package/app/components/tasks/TaskImportModal.vue +10 -8
  155. package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
  156. package/app/components/testing/TestReportWindow.vue +6 -12
  157. package/app/components/tutorial/TutorialCatalogue.vue +7 -3
  158. package/app/components/tutorial/TutorialOverlay.vue +2 -1
  159. package/app/components/tutorial/TutorialPrompt.vue +7 -3
  160. package/app/components/vcs/GitLabConnect.vue +2 -6
  161. package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
  162. package/app/composables/toastTitleKeys.spec.ts +143 -0
  163. package/app/composables/useActionToast.spec.ts +71 -0
  164. package/app/composables/useActionToast.ts +101 -0
  165. package/app/composables/useBlockDeletion.ts +3 -6
  166. package/app/composables/useConfirmAction.ts +5 -3
  167. package/app/composables/useContextLinking.spec.ts +3 -0
  168. package/app/composables/useContextLinking.ts +11 -16
  169. package/app/composables/useCopyToClipboard.ts +4 -3
  170. package/app/composables/useNavContributions.ts +4 -5
  171. package/app/composables/usePipelineErrorToast.spec.ts +24 -0
  172. package/app/composables/usePipelineErrorToast.ts +26 -2
  173. package/app/composables/usePipelineLibraryActions.ts +8 -9
  174. package/app/pages/index.vue +3 -2
  175. package/app/stores/board/context.ts +4 -3
  176. package/app/stores/board/placement.ts +8 -20
  177. package/app/stores/board/removal.ts +4 -13
  178. package/app/stores/board.spec.ts +10 -1
  179. package/app/stores/board.ts +3 -2
  180. package/app/utils/bootstrapSteps.ts +11 -2
  181. package/app/utils/pipelineRender.ts +21 -8
  182. package/i18n/locales/de.json +12 -30
  183. package/i18n/locales/en.json +12 -30
  184. package/i18n/locales/es.json +12 -30
  185. package/i18n/locales/fr.json +12 -30
  186. package/i18n/locales/he.json +12 -30
  187. package/i18n/locales/it.json +12 -30
  188. package/i18n/locales/ja.json +12 -30
  189. package/i18n/locales/pl.json +12 -30
  190. package/i18n/locales/tr.json +12 -30
  191. package/i18n/locales/uk.json +12 -30
  192. package/package.json +1 -1
@@ -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">
@@ -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 {
@@ -21,7 +21,7 @@ const props = defineProps<{ block: Block }>()
21
21
 
22
22
  const board = useBoardStore()
23
23
  const uiMode = useUiModeStore()
24
- const toast = useToast()
24
+ const actionToast = useActionToast()
25
25
  const { t } = useI18n()
26
26
 
27
27
  const busy = ref(false)
@@ -64,11 +64,7 @@ async function save() {
64
64
  // needs saying here; announcing it unconditionally would claim a save the rollback undid.
65
65
  const persisted = await board.updateBlock(props.block.id, { testingContext: outgoing.value })
66
66
  if (persisted) {
67
- toast.add({
68
- title: t('inspector.testingContext.savedToast'),
69
- icon: 'i-lucide-check',
70
- color: 'success',
71
- })
67
+ actionToast.success('inspector.testingContext.savedToast')
72
68
  }
73
69
  } finally {
74
70
  busy.value = false
@@ -10,6 +10,7 @@ import {
10
10
  type ValidationEcosystem,
11
11
  } from '~/types/validationChecks'
12
12
  import { mergeDetectedChecks } from '~/utils/validationDetection'
13
+ import EmptyState from '~/components/common/EmptyState.vue'
13
14
 
14
15
  // Per-service (frame) PRE-PR VALIDATION CHECKS: the shell commands the executor-harness runs
15
16
  // against the checkout after the coder settles and BEFORE the PR opens. A failing command's
@@ -19,7 +20,7 @@ import { mergeDetectedChecks } from '~/utils/validationDetection'
19
20
  const props = defineProps<{ block: Block }>()
20
21
 
21
22
  const store = useValidationChecksStore()
22
- const toast = useToast()
23
+ const actionToast = useActionToast()
23
24
  const { present } = usePipelineErrorToast()
24
25
  const { t, te } = useI18n()
25
26
  const { confirmAction, toastDone } = useConfirmAction()
@@ -104,11 +105,11 @@ async function detect() {
104
105
  if (result.status !== 'ok') {
105
106
  // The backend distinguishes "no repo linked" from "the repo could not be read"; say
106
107
  // which, because they send the operator to different places.
107
- toast.add({
108
- title: t(`inspector.validationChecks.detect.${result.status}`),
109
- icon: 'i-lucide-triangle-alert',
110
- color: 'warning',
111
- })
108
+ actionToast.error(
109
+ result.status === 'repo_unavailable'
110
+ ? 'inspector.validationChecks.detect.repo_unavailable'
111
+ : 'inspector.validationChecks.detect.failed',
112
+ )
112
113
  return
113
114
  }
114
115
  const merged = mergeDetectedChecks(rows.value, result.checks, VALIDATION_MAX_CHECKS)
@@ -120,39 +121,37 @@ async function detect() {
120
121
  const filledInstall = suggestedInstall !== '' && dependencyInstall.value.trim() === ''
121
122
  if (filledInstall) dependencyInstall.value = suggestedInstall
122
123
  if (merged.added === 0 && !filledInstall) {
123
- toast.add({
124
- title: t('inspector.validationChecks.detect.nothingNew'),
124
+ actionToast.info('inspector.validationChecks.detect.nothingNew', {
125
125
  description:
126
126
  result.checks.length > 0
127
127
  ? t('inspector.validationChecks.detect.alreadyPresent')
128
128
  : t('inspector.validationChecks.detect.unrecognised'),
129
- icon: 'i-lucide-info',
130
- color: 'neutral',
131
129
  })
132
130
  return
133
131
  }
134
132
  const names = result.ecosystems.map(ecosystemLabel).join(', ')
135
- toast.add({
136
- // An install-only detection fills nothing but the install field, and reporting it as
137
- // "0 checks added" would read as a failed press on the one repo shape prepopulation is
138
- // most for (dependencies to install, nothing declared to verify).
139
- title:
140
- merged.added === 0
141
- ? t('inspector.validationChecks.detect.installOnly')
142
- : t('inspector.validationChecks.detect.added', { count: merged.added }, merged.added),
143
- // Name what was recognised AND what was left out: a cap that silently swallowed a
144
- // suggestion reads as "that is everything your repo has".
145
- description: [
146
- names ? t('inspector.validationChecks.detect.found', { ecosystems: names }) : '',
147
- merged.dropped > 0 || result.truncated
148
- ? t('inspector.validationChecks.detect.capped', { max: VALIDATION_MAX_CHECKS })
149
- : '',
150
- ]
151
- .filter(Boolean)
152
- .join(' '),
153
- icon: 'i-lucide-wand-sparkles',
154
- color: 'success',
155
- })
133
+ // An install-only detection fills nothing but the install field, and reporting it as
134
+ // "0 checks added" would read as a failed press on the one repo shape prepopulation is
135
+ // most for (dependencies to install, nothing declared to verify).
136
+ actionToast.success(
137
+ merged.added === 0
138
+ ? 'inspector.validationChecks.detect.installOnly'
139
+ : 'inspector.validationChecks.detect.added',
140
+ {
141
+ params: { count: merged.added },
142
+ plural: merged.added === 0 ? undefined : merged.added,
143
+ // Name what was recognised AND what was left out: a cap that silently swallowed a
144
+ // suggestion reads as "that is everything your repo has".
145
+ description: [
146
+ names ? t('inspector.validationChecks.detect.found', { ecosystems: names }) : '',
147
+ merged.dropped > 0 || result.truncated
148
+ ? t('inspector.validationChecks.detect.capped', { max: VALIDATION_MAX_CHECKS })
149
+ : '',
150
+ ]
151
+ .filter(Boolean)
152
+ .join(' '),
153
+ },
154
+ )
156
155
  } catch (e) {
157
156
  present(e, 'inspector.validationChecks.detect.failed')
158
157
  } finally {
@@ -169,11 +168,7 @@ async function save() {
169
168
  maxAttempts.value,
170
169
  dependencyInstall.value.trim() || undefined,
171
170
  )
172
- toast.add({
173
- title: t('inspector.validationChecks.savedToast'),
174
- icon: 'i-lucide-check',
175
- color: 'success',
176
- })
171
+ actionToast.success('inspector.validationChecks.savedToast')
177
172
  } catch (e) {
178
173
  present(e, 'inspector.validationChecks.saveFailed')
179
174
  } finally {
@@ -273,9 +268,13 @@ async function clear() {
273
268
  />
274
269
  </div>
275
270
 
276
- <p v-if="rows.length === 0" class="text-2xs text-dimmed" data-testid="validation-empty">
277
- {{ t('inspector.validationChecks.empty') }}
278
- </p>
271
+ <EmptyState
272
+ v-if="rows.length === 0"
273
+ compact
274
+ icon="i-lucide-list-checks"
275
+ :title="t('inspector.validationChecks.empty')"
276
+ data-testid="validation-empty"
277
+ />
279
278
 
280
279
  <div class="flex items-end justify-between gap-2">
281
280
  <UFormField