@cat-factory/app 0.311.2 → 0.312.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 (210) hide show
  1. package/README.md +92 -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/board/nodes/TaskCard.vue +45 -212
  17. package/app/components/bootstrap/AdoptionReviewModal.vue +2 -5
  18. package/app/components/bootstrap/BootstrapModal.vue +6 -15
  19. package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
  20. package/app/components/brainstorm/BrainstormWindow.vue +32 -26
  21. package/app/components/bugFishing/BugFishingWindow.vue +15 -13
  22. package/app/components/clarity/ClarityReviewWindow.vue +31 -26
  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 +13 -7
  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 +27 -37
  75. package/app/components/layout/BoardTopOverlays.vue +8 -2
  76. package/app/components/layout/CommandBar.vue +7 -3
  77. package/app/components/layout/ConnectionStatusBanner.vue +2 -1
  78. package/app/components/layout/IntegrationsHub.vue +7 -3
  79. package/app/components/layout/ModelProvidersHub.vue +7 -3
  80. package/app/components/layout/NotificationsInbox.vue +3 -11
  81. package/app/components/layout/SideBar.vue +5 -0
  82. package/app/components/layout/WorkViewSwitcher.vue +64 -0
  83. package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
  84. package/app/components/media/ArtifactLightbox.vue +4 -3
  85. package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
  86. package/app/components/observability/ToolCallList.vue +12 -17
  87. package/app/components/panels/AgentStepDetail.vue +5 -5
  88. package/app/components/panels/GenericStructuredResultView.vue +6 -9
  89. package/app/components/panels/InspectorPanel.vue +2 -1
  90. package/app/components/panels/MergerResultView.vue +5 -6
  91. package/app/components/panels/ObservabilityPanel.vue +26 -41
  92. package/app/components/panels/OperatorDashboardPanel.vue +29 -17
  93. package/app/components/panels/ReportsPanel.vue +17 -11
  94. package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
  95. package/app/components/panels/RunDetailLoadState.vue +4 -3
  96. package/app/components/panels/StepContainerStatus.vue +4 -3
  97. package/app/components/panels/StepDelegatedStatus.vue +3 -2
  98. package/app/components/panels/StepMetadataCard.vue +6 -8
  99. package/app/components/panels/StepRunMeta.vue +2 -5
  100. package/app/components/panels/inspector/ContainerSummary.vue +7 -1
  101. package/app/components/panels/inspector/EpicChildren.vue +7 -3
  102. package/app/components/panels/inspector/FrontendConfig.vue +7 -3
  103. package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
  104. package/app/components/panels/inspector/ServiceConnections.vue +7 -3
  105. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
  106. package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
  107. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
  108. package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
  109. package/app/components/panels/inspector/TaskExecution.vue +5 -6
  110. package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
  111. package/app/components/panels/inspector/TaskStructure.vue +0 -1
  112. package/app/components/pipeline/AgentPromptEditor.vue +25 -18
  113. package/app/components/pipeline/PipelineBuilder.vue +14 -24
  114. package/app/components/pipeline/PipelineProgress.vue +16 -17
  115. package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
  116. package/app/components/prReview/PrReviewWindow.vue +16 -20
  117. package/app/components/providers/ApiKeysSection.vue +2 -6
  118. package/app/components/providers/PersonalCredentialModal.vue +2 -6
  119. package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
  120. package/app/components/providers/VendorCredentialsModal.vue +2 -6
  121. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
  122. package/app/components/queue/QueueCard.vue +269 -0
  123. package/app/components/queue/QueueView.vue +192 -0
  124. package/app/components/ralph/RalphLoopResultView.vue +2 -7
  125. package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
  126. package/app/components/sandbox/SandboxPanel.vue +37 -25
  127. package/app/components/settings/AccountSettingsPanel.vue +6 -3
  128. package/app/components/settings/ApiTokensPanel.vue +2 -6
  129. package/app/components/settings/BudgetSettings.vue +2 -2
  130. package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
  131. package/app/components/settings/CloudflareHandlerSection.vue +3 -7
  132. package/app/components/settings/ConsensusGroupsSection.vue +7 -3
  133. package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
  134. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
  135. package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
  136. package/app/components/settings/IssueTrackerPanel.vue +9 -9
  137. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  138. package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
  139. package/app/components/settings/McpAuthorizeScreen.vue +8 -3
  140. package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
  141. package/app/components/settings/ModelConfigurationPanel.vue +45 -17
  142. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
  143. package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
  144. package/app/components/settings/PackageRegistriesPanel.vue +2 -6
  145. package/app/components/settings/ProviderConnectionTab.vue +3 -7
  146. package/app/components/settings/RiskPolicyPanel.vue +4 -16
  147. package/app/components/settings/SharedStacksPanel.vue +8 -18
  148. package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
  149. package/app/components/settings/UsageSettings.vue +11 -2
  150. package/app/components/settings/UserSecretsSection.vue +5 -7
  151. package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
  152. package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
  153. package/app/components/skills/ReviewSkillQueue.vue +7 -3
  154. package/app/components/skills/SkillLibraryManager.vue +20 -18
  155. package/app/components/slack/SlackPanel.vue +5 -8
  156. package/app/components/spec/ServiceSpecWindow.vue +32 -29
  157. package/app/components/tasks/BugHuntModal.vue +23 -19
  158. package/app/components/tasks/ContextIssuePicker.vue +2 -2
  159. package/app/components/tasks/TaskContextIssues.vue +4 -5
  160. package/app/components/tasks/TaskImportModal.vue +10 -8
  161. package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
  162. package/app/components/testing/TestReportWindow.vue +6 -12
  163. package/app/components/tutorial/TutorialCatalogue.vue +7 -3
  164. package/app/components/tutorial/TutorialOverlay.vue +2 -1
  165. package/app/components/tutorial/TutorialPrompt.vue +7 -3
  166. package/app/components/vcs/GitLabConnect.vue +2 -6
  167. package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
  168. package/app/composables/toastTitleKeys.spec.ts +143 -0
  169. package/app/composables/useActionToast.spec.ts +71 -0
  170. package/app/composables/useActionToast.ts +101 -0
  171. package/app/composables/useBlockDeletion.ts +3 -6
  172. package/app/composables/useConfirmAction.ts +5 -3
  173. package/app/composables/useContextLinking.spec.ts +3 -0
  174. package/app/composables/useContextLinking.ts +11 -16
  175. package/app/composables/useCopyToClipboard.ts +4 -3
  176. package/app/composables/useFrameLanes.ts +6 -82
  177. package/app/composables/useNavContributions.ts +6 -5
  178. package/app/composables/usePipelineErrorToast.spec.ts +24 -0
  179. package/app/composables/usePipelineErrorToast.ts +26 -2
  180. package/app/composables/usePipelineLibraryActions.ts +8 -9
  181. package/app/composables/useTaskActions.ts +254 -0
  182. package/app/composables/useTaskLaneClassifier.ts +88 -0
  183. package/app/modular/nav-contributions.spec.ts +4 -0
  184. package/app/modular/nav-contributions.ts +31 -0
  185. package/app/modular/tutorial-tours.ts +10 -0
  186. package/app/pages/index.vue +42 -8
  187. package/app/stores/board/context.ts +4 -3
  188. package/app/stores/board/placement.ts +8 -20
  189. package/app/stores/board/removal.ts +4 -13
  190. package/app/stores/board.spec.ts +10 -1
  191. package/app/stores/board.ts +3 -2
  192. package/app/stores/workView.ts +51 -0
  193. package/app/stores/workspaceQueue.ts +148 -0
  194. package/app/utils/bootstrapSteps.ts +11 -2
  195. package/app/utils/pipelineRender.ts +21 -8
  196. package/app/utils/queueSections.spec.ts +111 -0
  197. package/app/utils/queueSections.ts +117 -0
  198. package/app/utils/workView.spec.ts +16 -0
  199. package/app/utils/workView.ts +25 -0
  200. package/i18n/locales/de.json +73 -32
  201. package/i18n/locales/en.json +73 -32
  202. package/i18n/locales/es.json +73 -32
  203. package/i18n/locales/fr.json +73 -32
  204. package/i18n/locales/he.json +73 -32
  205. package/i18n/locales/it.json +73 -32
  206. package/i18n/locales/ja.json +73 -32
  207. package/i18n/locales/pl.json +73 -32
  208. package/i18n/locales/tr.json +73 -32
  209. package/i18n/locales/uk.json +73 -32
  210. package/package.json +1 -1
@@ -23,6 +23,7 @@ import InputGateNotice from '~/components/inputGate/InputGateNotice.vue'
23
23
  import { inputGateNoticeFor } from '~/utils/inputGate'
24
24
  import { composeRunOutcome, hasOutcomeToShow } from '~/utils/runOutcome'
25
25
  import SectionLabel from '~/components/common/SectionLabel.vue'
26
+ import Spinner from '~/components/common/Spinner.vue'
26
27
 
27
28
  const props = defineProps<{ block: Block }>()
28
29
 
@@ -471,7 +472,7 @@ async function mergePr() {
471
472
  v-else-if="reviews.isBackground(s.agentKind, block.id) && reviewStage"
472
473
  class="inline-flex shrink-0 items-center gap-1 text-3xs text-primary"
473
474
  >
474
- <UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
475
+ <Spinner class="h-3 w-3" />
475
476
  {{ reviewStageLabel }}
476
477
  </span>
477
478
  <!-- A companion that spent its rework budget parks on the iteration-cap
@@ -591,13 +592,11 @@ async function mergePr() {
591
592
  v-if="gateCompanionFor(s, runFailed)"
592
593
  class="mt-0.5 flex items-center gap-1.5 ps-6 text-3xs"
593
594
  >
594
- <UIcon
595
+ <Spinner
595
596
  :name="agentKindMeta(gateCompanionFor(s, runFailed)!.kind).icon"
597
+ :spinning="gateCompanionFor(s, runFailed)!.state === 'running'"
596
598
  class="h-3 w-3 shrink-0"
597
- :class="[
598
- COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].text,
599
- gateCompanionFor(s, runFailed)!.state === 'running' ? 'animate-spin' : '',
600
- ]"
599
+ :class="COMPANION_STATE_META[gateCompanionFor(s, runFailed)!.state].text"
601
600
  />
602
601
  <span class="text-muted">
603
602
  {{
@@ -10,6 +10,7 @@ import RiskPolicyPicker from '~/components/riskPolicy/RiskPolicyPicker.vue'
10
10
  import TaskAprioriBranches from '~/components/panels/inspector/TaskAprioriBranches.vue'
11
11
  import DocReferenceRepos from '~/components/panels/inspector/DocReferenceRepos.vue'
12
12
  import SectionLabel from '~/components/common/SectionLabel.vue'
13
+ import EmptyState from '~/components/common/EmptyState.vue'
13
14
 
14
15
  const props = defineProps<{ block: Block }>()
15
16
 
@@ -503,9 +504,12 @@ const technicalLabel = computed(() => {
503
504
  @update:model-value="(v: boolean | 'indeterminate') => toggleInvolved(s.id, v === true)"
504
505
  />
505
506
  </div>
506
- <div v-else class="text-2xs text-dimmed">
507
- {{ t('inspector.runSettings.involvedServicesEmpty') }}
508
- </div>
507
+ <EmptyState
508
+ v-else
509
+ compact
510
+ icon="i-lucide-plug"
511
+ :title="t('inspector.runSettings.involvedServicesEmpty')"
512
+ />
509
513
  <div v-if="staleInvolvedServices.length" class="mt-1 flex flex-wrap gap-1">
510
514
  <UBadge
511
515
  v-for="id in staleInvolvedServices"
@@ -8,7 +8,6 @@ const props = defineProps<{ block: Block }>()
8
8
 
9
9
  const board = useBoardStore()
10
10
  const fragments = useFragmentsStore()
11
- const toast = useToast()
12
11
  const { t } = useI18n()
13
12
 
14
13
  // ---- module assignment -----------------------------------------------------
@@ -11,6 +11,7 @@ import {
11
11
  saveIntent,
12
12
  } from '~/components/pipeline/AgentPromptEditor.logic'
13
13
  import SectionLabel from '~/components/common/SectionLabel.vue'
14
+ import EmptyState from '~/components/common/EmptyState.vue'
14
15
 
15
16
  // The per-workspace system-prompt editor for ONE agent kind, opened from the pipeline builder
16
17
  // (where the kinds are actually chosen). It edits the SHIPPED track prompt only: the platform
@@ -30,7 +31,8 @@ const props = defineProps<{ agentKind: string | null }>()
30
31
  const emit = defineEmits<{ close: [] }>()
31
32
 
32
33
  const { t } = useI18n()
33
- const toast = useToast()
34
+ const actionToast = useActionToast()
35
+ const { present } = usePipelineErrorToast()
34
36
  const prompts = useAgentPromptsStore()
35
37
  const agentSettings = useAgentSettingsStore()
36
38
 
@@ -68,8 +70,8 @@ watch(
68
70
  try {
69
71
  const detail = await prompts.load(kind)
70
72
  draft.value = detail?.effectiveText ?? ''
71
- } catch {
72
- toast.add({ title: t('agentPrompt.toast.loadFailed'), color: 'error' })
73
+ } catch (error) {
74
+ present(error, 'agentPrompt.toast.loadFailed')
73
75
  emit('close')
74
76
  }
75
77
  },
@@ -97,8 +99,8 @@ async function saveBudget(value: number | null) {
97
99
  if (!kind) return
98
100
  try {
99
101
  await agentSettings.setMaxOutputTokens(kind, value)
100
- } catch {
101
- toast.add({ title: t('agentPrompt.toast.budgetFailed'), color: 'error' })
102
+ } catch (error) {
103
+ present(error, 'agentPrompt.toast.budgetFailed')
102
104
  }
103
105
  }
104
106
 
@@ -127,13 +129,13 @@ async function save() {
127
129
  const saved = await prompts.save(kind, intent.text, intent.restoredFrom)
128
130
  draft.value = saved?.effectiveText ?? draft.value
129
131
  restoredFrom.value = undefined
130
- toast.add({ title: t('agentPrompt.toast.saved'), color: 'success', icon: 'i-lucide-check' })
132
+ actionToast.success('agentPrompt.toast.saved')
131
133
  } catch (error) {
132
134
  const conflict = isRevisionConflict(error)
133
- toast.add({
134
- title: conflict ? t('agentPrompt.toast.conflict') : t('agentPrompt.toast.saveFailed'),
135
- color: 'error',
136
- })
135
+ // A recognised refusal keeps its bespoke copy: the funnel's generic line for this reason says
136
+ // "reload it", but the editor has already reloaded by the time the toast shows.
137
+ if (conflict) actionToast.error('agentPrompt.toast.conflict')
138
+ else present(error, 'agentPrompt.toast.saveFailed')
137
139
  // The server's view already replaced the store's on a conflict, so re-seed the textarea
138
140
  // from what actually landed rather than leaving the user editing a lost revision — and drop
139
141
  // the restore candidate with it, since it names a revision from the log we just replaced.
@@ -151,9 +153,9 @@ async function revert() {
151
153
  const saved = await prompts.save(kind, null)
152
154
  draft.value = saved?.effectiveText ?? draft.value
153
155
  restoredFrom.value = undefined
154
- toast.add({ title: t('agentPrompt.toast.reverted'), color: 'success' })
155
- } catch {
156
- toast.add({ title: t('agentPrompt.toast.saveFailed'), color: 'error' })
156
+ actionToast.success('agentPrompt.toast.reverted')
157
+ } catch (error) {
158
+ present(error, 'agentPrompt.toast.saveFailed')
157
159
  }
158
160
  }
159
161
 
@@ -175,8 +177,10 @@ function revisionLabel(revision: AgentPromptRevision): string {
175
177
  :ui="{ content: 'max-w-[92vw] sm:max-w-3xl lg:max-w-5xl' }"
176
178
  >
177
179
  <template #body>
178
- <div v-if="prompts.loadingDetail" class="py-8 text-center text-sm text-muted">
179
- {{ t('common.loading') }}
180
+ <div v-if="prompts.loadingDetail" class="flex flex-col gap-3">
181
+ <USkeleton class="h-6 w-1/3" />
182
+ <USkeleton class="h-64 w-full" />
183
+ <USkeleton class="h-24 w-full" />
180
184
  </div>
181
185
  <div v-else-if="detail" class="flex flex-col gap-3">
182
186
  <div class="flex flex-wrap items-center gap-2 text-xs">
@@ -296,9 +300,12 @@ function revisionLabel(revision: AgentPromptRevision): string {
296
300
  <SectionLabel as="h4" class="mb-1">
297
301
  {{ t('agentPrompt.historyHeading') }}
298
302
  </SectionLabel>
299
- <p v-if="!detail.revisions.length" class="text-2xs text-dimmed">
300
- {{ t('agentPrompt.historyEmpty') }}
301
- </p>
303
+ <EmptyState
304
+ v-if="!detail.revisions.length"
305
+ compact
306
+ icon="i-lucide-history"
307
+ :title="t('agentPrompt.historyEmpty')"
308
+ />
302
309
  <ul v-else class="max-h-52 divide-y divide-default overflow-y-auto text-xs">
303
310
  <li
304
311
  v-for="revision in detail.revisions"
@@ -30,6 +30,7 @@ import {
30
30
  import type { ConsensusStrategy } from '~/types/consensus'
31
31
  import SectionLabel from '~/components/common/SectionLabel.vue'
32
32
  import IconButton from '~/components/common/IconButton.vue'
33
+ import EmptyState from '~/components/common/EmptyState.vue'
33
34
 
34
35
  type DraftUnit = { index: number; kind: AgentKind; companionIndex: number | null }
35
36
 
@@ -280,7 +281,7 @@ function toggleSaved(id: string) {
280
281
  expandedSaved.value = next
281
282
  }
282
283
 
283
- const toast = useToast()
284
+ const actionToast = useActionToast()
284
285
  const { present } = usePipelineErrorToast()
285
286
 
286
287
  // ---- "Add agent" mini-form -------------------------------------------------
@@ -299,20 +300,12 @@ function createAgent() {
299
300
  label: newAgentName.value,
300
301
  description: newAgentDesc.value,
301
302
  })
302
- toast.add({
303
- title: t('pipeline.builder.toast.added', { name: agent.label }),
304
- color: 'success',
305
- icon: 'i-lucide-check',
306
- })
303
+ actionToast.success('pipeline.builder.toast.added', { params: { name: agent.label } })
307
304
  addAgentOpen.value = false
308
305
  }
309
306
 
310
307
  function placeholder(what: string) {
311
- toast.add({
312
- title: t('pipeline.builder.toast.placeholderTitle'),
313
- description: what,
314
- icon: 'i-lucide-construction',
315
- })
308
+ actionToast.info('pipeline.builder.toast.placeholderTitle', { description: what })
316
309
  }
317
310
 
318
311
  async function save() {
@@ -320,16 +313,13 @@ async function save() {
320
313
  try {
321
314
  const saved = await pipelines.saveDraft()
322
315
  if (saved) {
323
- toast.add({
324
- title: wasEditing
325
- ? t('pipeline.builder.toast.updated', { name: saved.name })
326
- : t('pipeline.builder.toast.saved', { name: saved.name }),
327
- color: 'success',
328
- icon: 'i-lucide-check',
329
- })
316
+ actionToast.success(
317
+ wasEditing ? 'pipeline.builder.toast.updated' : 'pipeline.builder.toast.saved',
318
+ { params: { name: saved.name } },
319
+ )
330
320
  ui.builderOpen = false
331
321
  } else {
332
- toast.add({ title: t('pipeline.builder.toast.addOneFirst'), color: 'warning' })
322
+ actionToast.error('pipeline.builder.toast.addOneFirst')
333
323
  }
334
324
  } catch (e) {
335
325
  // Through the funnel, so the backend reason (e.g. post-release-health rejected without an
@@ -521,12 +511,12 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
521
511
  {{ t(warning.key) }}
522
512
  </p>
523
513
 
524
- <div
514
+ <EmptyState
525
515
  v-if="pipelines.draft.length === 0"
526
- class="flex flex-1 items-center justify-center rounded-lg border border-dashed border-muted p-4 text-center text-xs text-dimmed"
527
- >
528
- {{ t('pipeline.builder.emptyDraft') }}
529
- </div>
516
+ icon="i-lucide-workflow"
517
+ :title="t('pipeline.builder.emptyDraft')"
518
+ class="flex-1"
519
+ />
530
520
 
531
521
  <ol v-else class="flex-1 space-y-2 pe-1 lg:min-h-0 lg:overflow-y-auto">
532
522
  <li
@@ -4,7 +4,8 @@ import type { PipelineStep } from '~/types/execution'
4
4
  import { stepHasOutput } from '@cat-factory/contracts'
5
5
  import { agentKindMeta, FOLLOW_UP_COMPANION_META, FORK_DECISION_META } from '~/utils/catalog'
6
6
  import {
7
- subtaskIconClass,
7
+ subtaskIconSpins,
8
+ subtaskIconTone,
8
9
  gateCompanionFor,
9
10
  COMPANION_STATE_META,
10
11
  isCompanionKind,
@@ -22,6 +23,7 @@ import { useNowTick, stepDurationLabel } from '~/composables/useStepTimer'
22
23
  import type { BadgeColor } from '~/utils/badge'
23
24
  import SectionLabel from '~/components/common/SectionLabel.vue'
24
25
  import IconButton from '~/components/common/IconButton.vue'
26
+ import Spinner from '~/components/common/Spinner.vue'
25
27
 
26
28
  const props = defineProps<{ instance: ExecutionInstance }>()
27
29
  const emit = defineEmits<{
@@ -321,10 +323,10 @@ const ITEM_ICON: Record<string, string> = {
321
323
  :class="liveWorking(s) ? 'step-active' : ''"
322
324
  :style="{ borderColor: stepVisual(s).color }"
323
325
  >
324
- <UIcon
326
+ <Spinner
325
327
  :name="stepVisual(s).icon"
328
+ :spinning="liveWorking(s)"
326
329
  class="h-4 w-4"
327
- :class="liveWorking(s) ? 'animate-spin' : ''"
328
330
  :style="{ color: stepVisual(s).color }"
329
331
  />
330
332
  </span>
@@ -460,7 +462,7 @@ const ITEM_ICON: Record<string, string> = {
460
462
  v-if="s.container?.status === 'starting' && !runFailed"
461
463
  class="mt-2 flex items-center gap-1.5 text-2xs text-app-info-300"
462
464
  >
463
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 shrink-0 animate-spin" />
465
+ <Spinner class="h-3.5 w-3.5 shrink-0" />
464
466
  <span>{{ t('pipeline.progress.spinningUpContainer') }}</span>
465
467
  </div>
466
468
 
@@ -524,10 +526,11 @@ const ITEM_ICON: Record<string, string> = {
524
526
  : 'text-muted'
525
527
  "
526
528
  >
527
- <UIcon
529
+ <Spinner
528
530
  :name="ITEM_ICON[item.status]"
531
+ :spinning="subtaskIconSpins(item.status, runFailed)"
529
532
  class="mt-px h-3 w-3 shrink-0"
530
- :class="subtaskIconClass(item.status, runFailed)"
533
+ :class="subtaskIconTone(item.status)"
531
534
  />
532
535
  <span>{{ item.label }}</span>
533
536
  </li>
@@ -584,13 +587,11 @@ const ITEM_ICON: Record<string, string> = {
584
587
  class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border"
585
588
  :class="COMPANION_STATE_META[companionByStep[i]!.state].dot"
586
589
  >
587
- <UIcon
590
+ <Spinner
588
591
  :name="agentKindMeta(companionByStep[i]!.kind).icon"
592
+ :spinning="companionByStep[i]!.state === 'running' && !runFailed"
589
593
  class="h-3 w-3"
590
- :class="[
591
- COMPANION_STATE_META[companionByStep[i]!.state].text,
592
- companionByStep[i]!.state === 'running' && !runFailed ? 'animate-spin' : '',
593
- ]"
594
+ :class="COMPANION_STATE_META[companionByStep[i]!.state].text"
594
595
  />
595
596
  </span>
596
597
  <span class="min-w-0 flex-1 truncate text-xs text-toned">
@@ -657,12 +658,10 @@ const ITEM_ICON: Record<string, string> = {
657
658
  <span
658
659
  class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-app-secondary-500/40 bg-app-secondary-500/15"
659
660
  >
660
- <UIcon
661
- :name="
662
- forkPhase(s) === 'proposing' ? 'i-lucide-loader-circle' : FORK_DECISION_META.icon
663
- "
661
+ <Spinner
662
+ :name="forkPhase(s) === 'proposing' ? undefined : FORK_DECISION_META.icon"
663
+ :spinning="forkPhase(s) === 'proposing'"
664
664
  class="h-3 w-3 text-app-secondary-300"
665
- :class="forkPhase(s) === 'proposing' ? 'animate-spin' : ''"
666
665
  />
667
666
  </span>
668
667
  <span class="min-w-0 flex-1 truncate text-xs text-toned">
@@ -727,7 +726,7 @@ const ITEM_ICON: Record<string, string> = {
727
726
  v-if="reviewStageLabel(s.agentKind)"
728
727
  class="mt-3 inline-flex items-center gap-1 text-2xs text-primary"
729
728
  >
730
- <UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
729
+ <Spinner class="h-3 w-3" />
731
730
  {{ reviewStageLabel(s.agentKind) }}
732
731
  </div>
733
732
 
@@ -2,6 +2,7 @@
2
2
  import { computed } from 'vue'
3
3
  import type { PipelineStep } from '~/types/execution'
4
4
  import { prReviewPhase, type PrReviewPhaseKind } from '~/utils/prReviewProgress'
5
+ import Spinner from '~/components/common/Spinner.vue'
5
6
 
6
7
  // Compact, at-a-glance phase label for a live `pr-reviewer` step, so the BOARD surfaces (the
7
8
  // task-card mini pipeline + the focus-view timeline) tell the reviewer's sub-phase apart —
@@ -64,10 +65,10 @@ const spinning = computed(
64
65
  class="inline-flex items-center gap-1"
65
66
  :class="runFailed ? 'text-app-error-400' : PHASE_META[phase.kind].class"
66
67
  >
67
- <UIcon
68
+ <Spinner
68
69
  :name="runFailed ? 'i-lucide-circle-x' : PHASE_META[phase.kind].icon"
70
+ :spinning="spinning"
69
71
  class="h-3 w-3 shrink-0"
70
- :class="spinning ? 'animate-spin' : ''"
71
72
  />
72
73
  <span class="truncate">{{ label }}</span>
73
74
  </span>
@@ -24,13 +24,15 @@ import type {
24
24
  StepSubtaskItem,
25
25
  StepSubtasks,
26
26
  } from '~/types/execution'
27
- import { subtaskIconClass } from '~/utils/pipelineRender'
27
+ import { subtaskIconSpins, subtaskIconTone } from '~/utils/pipelineRender'
28
28
  import { activeChunkLabels, chunkReviewPercent, hasNoSlicePlan } from '~/utils/prReviewProgress'
29
29
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
30
30
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
31
31
  import StepFragmentAdherence from '~/components/panels/StepFragmentAdherence.vue'
32
32
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
33
33
  import SectionLabel from '~/components/common/SectionLabel.vue'
34
+ import Spinner from '~/components/common/Spinner.vue'
35
+ import EmptyState from '~/components/common/EmptyState.vue'
34
36
 
35
37
  const execution = useExecutionStore()
36
38
  const board = useBoardStore()
@@ -320,7 +322,7 @@ const { requestClose } = useUnsavedGuard({
320
322
  data-testid="pr-review-planning"
321
323
  class="flex flex-1 flex-col items-center justify-center gap-2 py-10 text-center text-muted"
322
324
  >
323
- <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
325
+ <Spinner class="h-8 w-8 opacity-60" />
324
326
  <p class="text-sm text-default">{{ t('prReview.reviewing.planning.title') }}</p>
325
327
  <p class="max-w-sm text-2xs text-dimmed">
326
328
  {{ t('prReview.reviewing.planning.hint') }}
@@ -330,10 +332,7 @@ const { requestClose } = useUnsavedGuard({
330
332
  <!-- REVIEWING: a per-slice plan exists — show every slice with its status + which are active now. -->
331
333
  <div v-else data-testid="pr-review-reviewing-chunks" class="py-2">
332
334
  <div class="mb-1 flex items-center gap-2 text-sm text-default">
333
- <UIcon
334
- name="i-lucide-loader-circle"
335
- class="h-4 w-4 shrink-0 animate-spin text-primary"
336
- />
335
+ <Spinner class="h-4 w-4 shrink-0 text-primary" />
337
336
  <span>{{ t('prReview.reviewing.reviewingChunks.title') }}</span>
338
337
  </div>
339
338
  <p class="mb-3 text-2xs text-dimmed">
@@ -385,10 +384,7 @@ const { requestClose } = useUnsavedGuard({
385
384
  :key="i"
386
385
  class="flex items-start gap-1.5 text-xs text-app-100"
387
386
  >
388
- <UIcon
389
- name="i-lucide-loader-circle"
390
- class="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin text-primary"
391
- />
387
+ <Spinner class="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" />
392
388
  <span class="min-w-0">{{ label }}</span>
393
389
  </li>
394
390
  </ul>
@@ -416,10 +412,11 @@ const { requestClose } = useUnsavedGuard({
416
412
  : 'text-muted'
417
413
  "
418
414
  >
419
- <UIcon
415
+ <Spinner
420
416
  :name="ITEM_ICON[item.status]"
417
+ :spinning="subtaskIconSpins(item.status, false)"
421
418
  class="h-3.5 w-3.5 shrink-0"
422
- :class="subtaskIconClass(item.status, false)"
419
+ :class="subtaskIconTone(item.status)"
423
420
  />
424
421
  <span
425
422
  class="min-w-0 flex-1 truncate"
@@ -477,7 +474,7 @@ const { requestClose } = useUnsavedGuard({
477
474
  data-testid="pr-review-working"
478
475
  class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-muted"
479
476
  >
480
- <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
477
+ <Spinner class="h-8 w-8 opacity-60" />
481
478
  <p class="text-sm">
482
479
  {{ status === 'fixing' ? t('prReview.fixing.title') : t('prReview.posting.title') }}
483
480
  </p>
@@ -564,12 +561,11 @@ const { requestClose } = useUnsavedGuard({
564
561
  />
565
562
 
566
563
  <!-- A clean PR / resolved review with no findings. -->
567
- <div
564
+ <EmptyState
568
565
  v-if="findings.length === 0"
569
- class="rounded-xl border border-default bg-default/60 px-4 py-6 text-center text-sm text-toned"
570
- >
571
- {{ t('prReview.noFindings') }}
572
- </div>
566
+ icon="i-lucide-circle-check"
567
+ :title="t('prReview.noFindings')"
568
+ />
573
569
 
574
570
  <template v-else>
575
571
  <!-- A challenge is in flight: the Challenge Investigator is re-examining a finding. -->
@@ -578,7 +574,7 @@ const { requestClose } = useUnsavedGuard({
578
574
  data-testid="pr-review-challenging"
579
575
  class="mb-3 flex items-center gap-2 rounded-md border border-primary/40 bg-primary/10 px-3 py-2 text-xs text-primary"
580
576
  >
581
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 shrink-0 animate-spin" />
577
+ <Spinner class="h-4 w-4 shrink-0" />
582
578
  <span>{{ t('prReview.challenge.investigatingBanner') }}</span>
583
579
  </div>
584
580
 
@@ -685,7 +681,7 @@ const { requestClose } = useUnsavedGuard({
685
681
  data-testid="pr-review-finding-investigating"
686
682
  class="flex items-center gap-1 rounded-sm bg-primary/15 px-1.5 py-0.5 text-3xs font-semibold uppercase text-primary ring-1 ring-primary/30"
687
683
  >
688
- <UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
684
+ <Spinner class="h-3 w-3" />
689
685
  {{ t('prReview.challenge.investigatingBadge') }}
690
686
  </span>
691
687
  <h4
@@ -30,7 +30,7 @@ const workspace = useWorkspaceStore()
30
30
  const keys = useApiKeysStore()
31
31
  const models = useModelsStore()
32
32
  const auth = useAuthStore()
33
- const toast = useToast()
33
+ const actionToast = useActionToast()
34
34
  const { present } = usePipelineErrorToast()
35
35
  const { confirmAction, toastDone } = useConfirmAction()
36
36
 
@@ -241,11 +241,7 @@ async function add() {
241
241
  label.value = ''
242
242
  // The picker's selectability depends on configured keys — refresh it.
243
243
  if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
244
- toast.add({
245
- title: t('providers.apiKeys.toast.connected'),
246
- icon: 'i-lucide-check',
247
- color: 'success',
248
- })
244
+ actionToast.success('providers.apiKeys.toast.connected')
249
245
  } catch (e) {
250
246
  present(e, 'providers.apiKeys.toast.connectFailed')
251
247
  } finally {
@@ -10,7 +10,7 @@ import SecretInput from '~/components/common/SecretInput.vue'
10
10
  const { t } = useI18n()
11
11
  const personal = usePersonalSubscriptionsStore()
12
12
  const ui = useUiStore()
13
- const toast = useToast()
13
+ const actionToast = useActionToast()
14
14
  const { present } = usePipelineErrorToast()
15
15
 
16
16
  const password = ref('')
@@ -67,11 +67,7 @@ async function submit() {
67
67
  busy.value = true
68
68
  try {
69
69
  await pending.value.retry(password.value)
70
- toast.add({
71
- title: t('providers.personalCredential.toast.started'),
72
- icon: 'i-lucide-check',
73
- color: 'success',
74
- })
70
+ actionToast.success('providers.personalCredential.toast.started')
75
71
  } catch (e) {
76
72
  // A fresh 428 (e.g. still-wrong password) re-arms `pending`, keeping the modal open.
77
73
  present(e, 'providers.personalCredential.toast.startFailed')
@@ -13,7 +13,7 @@ const personal = usePersonalSubscriptionsStore()
13
13
  const auth = useAuthStore()
14
14
  const workspace = useWorkspaceStore()
15
15
  const models = useModelsStore()
16
- const toast = useToast()
16
+ const actionToast = useActionToast()
17
17
  const { present } = usePipelineErrorToast()
18
18
  const { t, d } = useI18n()
19
19
  const { confirm } = useConfirm()
@@ -173,11 +173,7 @@ async function connect() {
173
173
  // points at models this subscription doesn't cover, reactively surfaces the
174
174
  // preset-mismatch prompt (with its "pick a different preset" link).
175
175
  if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
176
- toast.add({
177
- title: t('personalSubscriptions.toast.connected', { vendor: vendorName }),
178
- icon: 'i-lucide-check',
179
- color: 'success',
180
- })
176
+ actionToast.success('personalSubscriptions.toast.connected', { params: { vendor: vendorName } })
181
177
  } catch (e) {
182
178
  present(e, 'personalSubscriptions.toast.connectFailed')
183
179
  } finally {
@@ -199,7 +195,7 @@ async function disconnect(v: SubscriptionVendor) {
199
195
  // Removing the subscription may drop the workspace's last usable model — refresh so the
200
196
  // AI-readiness banners re-evaluate (mirrors the API-key flow).
201
197
  if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
202
- toast.add({ title: t('personalSubscriptions.toast.disconnected'), icon: 'i-lucide-check' })
198
+ actionToast.success('personalSubscriptions.toast.disconnected')
203
199
  } catch (e) {
204
200
  present(e, 'personalSubscriptions.toast.disconnectFailed')
205
201
  }
@@ -15,7 +15,7 @@ const { t, n } = useI18n()
15
15
  const ui = useUiStore()
16
16
  const workspace = useWorkspaceStore()
17
17
  const creds = useVendorCredentialsStore()
18
- const toast = useToast()
18
+ const actionToast = useActionToast()
19
19
  const { present } = usePipelineErrorToast()
20
20
  const { confirm } = useConfirm()
21
21
 
@@ -127,11 +127,7 @@ async function add() {
127
127
  })
128
128
  token.value = ''
129
129
  label.value = ''
130
- toast.add({
131
- title: t('providers.vendorCredentials.toast.connected'),
132
- icon: 'i-lucide-check',
133
- color: 'success',
134
- })
130
+ actionToast.success('providers.vendorCredentials.toast.connected')
135
131
  } catch (e) {
136
132
  present(e, 'providers.vendorCredentials.toast.connectFailed')
137
133
  } finally {
@@ -23,6 +23,7 @@ import type {
23
23
  ProvisioningSubsystem,
24
24
  } from '~/types/provisioningLogs'
25
25
  import SectionLabel from '~/components/common/SectionLabel.vue'
26
+ import EmptyState from '~/components/common/EmptyState.vue'
26
27
 
27
28
  const props = defineProps<{
28
29
  subsystem?: ProvisioningSubsystem
@@ -132,12 +133,15 @@ function when(epochMs: number): string {
132
133
  </div>
133
134
 
134
135
  <p v-if="state.error" class="px-3 py-2 text-xs text-app-error-300">{{ state.error }}</p>
135
- <p
136
- v-else-if="!state.loading && state.entries.length === 0"
137
- class="px-3 py-3 text-xs text-dimmed"
138
- >
139
- {{ t('provisioning.empty') }}
140
- </p>
136
+ <div v-else-if="state.loading && state.entries.length === 0" class="space-y-2 px-3 py-2">
137
+ <USkeleton v-for="n in 3" :key="n" class="h-8 w-full" />
138
+ </div>
139
+ <EmptyState
140
+ v-else-if="state.entries.length === 0"
141
+ compact
142
+ icon="i-lucide-scroll-text"
143
+ :title="t('provisioning.empty')"
144
+ />
141
145
 
142
146
  <ul v-else class="max-h-80 divide-y divide-default overflow-auto">
143
147
  <li v-for="entry in state.entries" :key="entry.id" class="px-3 py-2">