@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
@@ -4,6 +4,8 @@
4
4
  import { isPostableDraft } from '@cat-factory/contracts'
5
5
  import type { GuidedReviewCommentDraft, GuidedReviewThreadView } from '~/types/domain'
6
6
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
7
+ import Spinner from '~/components/common/Spinner.vue'
8
+ import EmptyState from '~/components/common/EmptyState.vue'
7
9
  import GuidedReviewFailure from '~/components/guidedReview/GuidedReviewFailure.vue'
8
10
  import GuidedReviewDraftCard from '~/components/guidedReview/GuidedReviewDraftCard.vue'
9
11
  import {
@@ -112,9 +114,12 @@ async function draftComments(): Promise<void> {
112
114
  <template>
113
115
  <div class="flex h-full min-h-0 flex-col gap-3" data-testid="guided-review-thread">
114
116
  <div class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
115
- <p v-if="!messages.length" class="text-sm text-muted">
116
- {{ t('guidedReview.thread.empty') }}
117
- </p>
117
+ <EmptyState
118
+ v-if="!messages.length"
119
+ compact
120
+ icon="i-lucide-message-circle-question"
121
+ :title="t('guidedReview.thread.empty')"
122
+ />
118
123
  <div
119
124
  v-for="m in messages"
120
125
  :key="m.id"
@@ -129,7 +134,7 @@ async function draftComments(): Promise<void> {
129
134
  <p class="whitespace-pre-wrap">{{ m.content }}</p>
130
135
  </template>
131
136
  <p v-else-if="isLive(m)" class="flex items-center gap-2 text-muted">
132
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
137
+ <Spinner class="h-4 w-4" />
133
138
  {{
134
139
  m.depth === 'deep'
135
140
  ? t('guidedReview.thread.investigating')
@@ -16,6 +16,7 @@ type HumanTestRoundOutcome = NonNullable<HumanTestRound['outcome']>
16
16
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
17
17
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
18
18
  import SectionLabel from '~/components/common/SectionLabel.vue'
19
+ import Spinner from '~/components/common/Spinner.vue'
19
20
 
20
21
  const board = useBoardStore()
21
22
  const execution = useExecutionStore()
@@ -253,7 +254,7 @@ const canDestroy = computed(
253
254
  v-if="working"
254
255
  class="flex items-center gap-2 rounded-lg border border-default bg-app-950/40 px-3 py-2 text-xs text-toned"
255
256
  >
256
- <UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-app-warning-300" />
257
+ <Spinner class="h-3.5 w-3.5 text-app-warning-300" />
257
258
  {{ phase ? t(PHASE_LABEL[phase]) : '' }}
258
259
  </p>
259
260
 
@@ -37,6 +37,7 @@ import {
37
37
  } from '~/utils/interviewGate'
38
38
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
39
39
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
40
+ import EmptyState from '~/components/common/EmptyState.vue'
40
41
 
41
42
  const board = useBoardStore()
42
43
  const initiatives = useInitiativesStore()
@@ -212,13 +213,12 @@ async function onDiscard() {
212
213
  <div class="flex min-h-0 flex-1">
213
214
  <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
214
215
  <!-- No entity yet -->
215
- <div
216
+ <EmptyState
216
217
  v-if="!initiative"
217
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
218
- >
219
- <UIcon name="i-lucide-messages-square" class="h-8 w-8 opacity-40" />
220
- <p class="text-sm">{{ t('initiative.planning.empty') }}</p>
221
- </div>
218
+ class="h-full"
219
+ icon="i-lucide-messages-square"
220
+ :title="t('initiative.planning.empty')"
221
+ />
222
222
 
223
223
  <template v-else>
224
224
  <p class="mb-4 text-sm leading-relaxed text-toned">
@@ -31,6 +31,7 @@ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
31
31
  import InitiativePlanReview from '~/components/initiative/InitiativePlanReview.vue'
32
32
  import InitiativePlanNotice from '~/components/initiative/InitiativePlanNotice.vue'
33
33
  import SectionLabel from '~/components/common/SectionLabel.vue'
34
+ import EmptyState from '~/components/common/EmptyState.vue'
34
35
 
35
36
  const board = useBoardStore()
36
37
  const initiatives = useInitiativesStore()
@@ -373,14 +374,12 @@ const { requestClose } = useUnsavedGuard({
373
374
  <!-- No entity yet (module unwired / still creating). Centred in the column when it is the
374
375
  only thing in it; merely inset when the notice above it means `h-full` would overflow
375
376
  the scroller by the notice's own height. -->
376
- <div
377
+ <EmptyState
377
378
  v-if="!initiative"
378
- class="flex flex-col items-center justify-center gap-2 text-center text-muted"
379
379
  :class="planApproval ? 'py-16' : 'h-full'"
380
- >
381
- <UIcon name="i-lucide-milestone" class="h-8 w-8 opacity-40" />
382
- <p class="text-sm">{{ t('initiative.tracker.empty') }}</p>
383
- </div>
380
+ icon="i-lucide-milestone"
381
+ :title="t('initiative.tracker.empty')"
382
+ />
384
383
 
385
384
  <template v-else>
386
385
  <!-- Paused at a phase checkpoint (D2): a completed checkpoint phase is awaiting
@@ -467,12 +466,13 @@ const { requestClose } = useUnsavedGuard({
467
466
  </section>
468
467
 
469
468
  <!-- Awaiting planning -->
470
- <div
469
+ <EmptyState
471
470
  v-if="phases.length === 0"
472
- class="mb-4 rounded-lg border border-dashed border-muted p-4 text-center text-xs text-muted"
473
- >
474
- {{ t('initiative.tracker.noPlan') }}
475
- </div>
471
+ class="mb-4"
472
+ compact
473
+ icon="i-lucide-list-todo"
474
+ :title="t('initiative.tracker.noPlan')"
475
+ />
476
476
 
477
477
  <!-- Phases + items -->
478
478
  <section v-for="phase in phases" :key="phase.id" class="mb-5">
@@ -14,6 +14,7 @@ import ResultWindowShell from '~/components/panels/ResultWindowShell.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 EmptyState from '~/components/common/EmptyState.vue'
17
18
 
18
19
  const board = useBoardStore()
19
20
  const execution = useExecutionStore()
@@ -149,13 +150,7 @@ async function act(choice: 'proceed' | 'bounce' | 'stop') {
149
150
  </template>
150
151
 
151
152
  <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
152
- <div
153
- v-if="!judge"
154
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
155
- >
156
- <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
157
- <p class="text-sm">{{ t('judge.empty') }}</p>
158
- </div>
153
+ <EmptyState v-if="!judge" :icon="meta.icon" :title="t('judge.empty')" />
159
154
 
160
155
  <template v-else>
161
156
  <!-- The score against the task's threshold — the whole verdict in one line. -->
@@ -4,6 +4,7 @@ import { onKeyStroke } from '@vueuse/core'
4
4
  import type { TableColumn } from '@nuxt/ui'
5
5
  import type { KaizenGrading } from '~/types/domain'
6
6
  import { agentKindMeta } from '~/utils/catalog'
7
+ import EmptyState from '~/components/common/EmptyState.vue'
7
8
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
8
9
 
9
10
  // The Kaizen screen: a full-panel overlay listing the workspace's grading history and
@@ -123,7 +124,16 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
123
124
  <p class="mb-3 text-2xs text-dimmed">
124
125
  {{ t('kaizen.verifiedCombos.hint') }}
125
126
  </p>
126
- <ul class="space-y-2">
127
+ <div v-if="kaizen.loadingOverview && kaizen.verified.length === 0" class="space-y-2">
128
+ <USkeleton v-for="n in 3" :key="n" class="h-14 w-full rounded-lg" />
129
+ </div>
130
+ <EmptyState
131
+ v-else-if="kaizen.verified.length === 0"
132
+ compact
133
+ icon="i-lucide-badge-check"
134
+ :title="t('kaizen.verifiedCombos.empty')"
135
+ />
136
+ <ul v-else class="space-y-2">
127
137
  <li
128
138
  v-for="c in kaizen.verified"
129
139
  :key="c.comboKey"
@@ -156,9 +166,6 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
156
166
  }}
157
167
  </div>
158
168
  </li>
159
- <li v-if="kaizen.verified.length === 0" class="text-xs text-app-600">
160
- {{ t('kaizen.verifiedCombos.empty') }}
161
- </li>
162
169
  </ul>
163
170
  </section>
164
171
 
@@ -171,6 +178,7 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
171
178
  <UTable
172
179
  :data="kaizen.history"
173
180
  :columns="historyColumns"
181
+ :loading="kaizen.loadingOverview"
174
182
  :ui="{
175
183
  root: 'overflow-hidden rounded-lg border border-default',
176
184
  base: 'text-xs',
@@ -220,8 +228,15 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
220
228
  </span>
221
229
  </div>
222
230
  </template>
231
+ <!-- UTable draws its loading row only through this slot; without it an empty first
232
+ load falls through to #empty and claims there is no history. -->
233
+ <template #loading>
234
+ <div class="space-y-2">
235
+ <USkeleton v-for="n in 3" :key="n" class="h-8 w-full" />
236
+ </div>
237
+ </template>
223
238
  <template #empty>
224
- <span class="text-app-600">{{ t('kaizen.history.empty') }}</span>
239
+ <EmptyState compact icon="i-lucide-history" :title="t('kaizen.history.empty')" />
225
240
  </template>
226
241
  </UTable>
227
242
  </section>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import SectionLabel from '~/components/common/SectionLabel.vue'
4
+ import Spinner from '~/components/common/Spinner.vue'
4
5
 
5
6
  // Per-step Kaizen grading status, shown inside the run window (NOT on the board). Reads
6
7
  // the grading for this run's step from the kaizen store, lazily loading the run's
@@ -54,7 +55,7 @@ const tone = computed(() => {
54
55
  <span class="text-muted">{{ t('kaizen.status.scheduled') }}</span>
55
56
  </template>
56
57
  <template v-else-if="grading.status === 'running'">
57
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin text-app-hue-teal" />
58
+ <Spinner class="h-3.5 w-3.5 text-app-hue-teal" />
58
59
  <span class="text-app-hue-teal">{{ t('kaizen.status.grading') }}</span>
59
60
  </template>
60
61
  <template v-else-if="grading.status === 'failed'">
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue'
3
3
  import { apiErrorEnvelope } from '~/composables/api/errors'
4
4
  import type { AuditEventWire } from '@cat-factory/contracts'
5
5
  import { actorLabel, describeEvent } from './AccountAuditLog.logic'
6
+ import EmptyState from '~/components/common/EmptyState.vue'
6
7
 
7
8
  // The account audit log: who did what, when, for the privileged actions an account admin is
8
9
  // answerable for. Read-only by construction — the store exposes no mutation and the backend has
@@ -109,9 +110,16 @@ function timestamp(at: number): string {
109
110
  <span class="text-muted">{{ loadError }}</span>
110
111
  </p>
111
112
 
112
- <p v-else-if="events.length === 0 && !loading" class="text-muted">
113
- {{ t('layout.auditLog.empty') }}
114
- </p>
113
+ <div v-else-if="events.length === 0 && loading" class="space-y-2">
114
+ <USkeleton v-for="i in 3" :key="i" class="h-14 w-full" />
115
+ </div>
116
+
117
+ <EmptyState
118
+ v-else-if="events.length === 0"
119
+ compact
120
+ icon="i-lucide-scroll-text"
121
+ :title="t('layout.auditLog.empty')"
122
+ />
115
123
 
116
124
  <ol v-else class="space-y-2" data-testid="audit-log-list">
117
125
  <li
@@ -13,7 +13,7 @@ const props = defineProps<{ accountId: string }>()
13
13
 
14
14
  const store = useAccountSettingsStore()
15
15
  const ui = useUiStore()
16
- const toast = useToast()
16
+ const actionToast = useActionToast()
17
17
  const { present } = usePipelineErrorToast()
18
18
  const { t } = useI18n()
19
19
  const { confirmAction } = useConfirmAction()
@@ -178,10 +178,7 @@ async function saveStorage() {
178
178
  }
179
179
  if (backend === 's3') {
180
180
  if (!cs.region.trim() || !cs.bucket.trim()) {
181
- toast.add({
182
- title: t('layout.accountDeployment.contentStorage.regionBucketValidation'),
183
- color: 'error',
184
- })
181
+ actionToast.error('layout.accountDeployment.contentStorage.regionBucketValidation')
185
182
  return
186
183
  }
187
184
  config.s3 = {
@@ -203,16 +200,10 @@ async function saveStorage() {
203
200
  if (id && key) {
204
201
  input.secrets = { s3: { accessKeyId: id, secretAccessKey: key } }
205
202
  } else if (id || key) {
206
- toast.add({
207
- title: t('layout.accountDeployment.contentStorage.bothKeysValidation'),
208
- color: 'error',
209
- })
203
+ actionToast.error('layout.accountDeployment.contentStorage.bothKeysValidation')
210
204
  return
211
205
  } else if (!storageSummary.value?.s3CredentialsConfigured) {
212
- toast.add({
213
- title: t('layout.accountDeployment.contentStorage.keysValidation'),
214
- color: 'error',
215
- })
206
+ actionToast.error('layout.accountDeployment.contentStorage.keysValidation')
216
207
  return
217
208
  }
218
209
  // else: keys already stored and none re-entered → leave them unchanged.
@@ -224,11 +215,7 @@ async function saveStorage() {
224
215
  try {
225
216
  await store.save(props.accountId, input)
226
217
  hydrateStorage()
227
- toast.add({
228
- title: t('layout.accountDeployment.contentStorage.saved'),
229
- icon: 'i-lucide-check',
230
- color: 'success',
231
- })
218
+ actionToast.success('layout.accountDeployment.contentStorage.saved')
232
219
  } catch (e) {
233
220
  present(e, 'layout.accountDeployment.contentStorage.saveFailed')
234
221
  } finally {
@@ -238,7 +225,7 @@ async function saveStorage() {
238
225
 
239
226
  async function saveSlack() {
240
227
  if (!slack.clientId.trim() || !slack.clientSecret.trim() || !slack.redirectUrl.trim()) {
241
- toast.add({ title: t('layout.accountDeployment.slack.validation'), color: 'error' })
228
+ actionToast.error('layout.accountDeployment.slack.validation')
242
229
  return
243
230
  }
244
231
  savingSlack.value = true
@@ -255,11 +242,7 @@ async function saveSlack() {
255
242
  slack.clientId = ''
256
243
  slack.clientSecret = ''
257
244
  slack.redirectUrl = ''
258
- toast.add({
259
- title: t('layout.accountDeployment.slack.saved'),
260
- icon: 'i-lucide-check',
261
- color: 'success',
262
- })
245
+ actionToast.success('layout.accountDeployment.slack.saved')
263
246
  } catch (e) {
264
247
  present(e, 'layout.accountDeployment.slack.saveFailed')
265
248
  } finally {
@@ -272,11 +255,7 @@ async function clearSlack() {
272
255
  savingSlack.value = true
273
256
  try {
274
257
  await store.save(props.accountId, { secrets: { slackOAuth: null } })
275
- toast.add({
276
- title: t('layout.accountDeployment.slack.cleared'),
277
- icon: 'i-lucide-check',
278
- color: 'success',
279
- })
258
+ actionToast.success('layout.accountDeployment.slack.cleared')
280
259
  } catch (e) {
281
260
  present(e, 'layout.accountDeployment.slack.clearFailed')
282
261
  } finally {
@@ -286,7 +265,7 @@ async function clearSlack() {
286
265
 
287
266
  async function saveLinear() {
288
267
  if (!linear.clientId.trim() || !linear.clientSecret.trim() || !linear.redirectUrl.trim()) {
289
- toast.add({ title: t('layout.accountDeployment.linear.validation'), color: 'error' })
268
+ actionToast.error('layout.accountDeployment.linear.validation')
290
269
  return
291
270
  }
292
271
  savingLinear.value = true
@@ -303,11 +282,7 @@ async function saveLinear() {
303
282
  linear.clientId = ''
304
283
  linear.clientSecret = ''
305
284
  linear.redirectUrl = ''
306
- toast.add({
307
- title: t('layout.accountDeployment.linear.saved'),
308
- icon: 'i-lucide-check',
309
- color: 'success',
310
- })
285
+ actionToast.success('layout.accountDeployment.linear.saved')
311
286
  } catch (e) {
312
287
  present(e, 'layout.accountDeployment.linear.saveFailed')
313
288
  } finally {
@@ -320,11 +295,7 @@ async function clearLinear() {
320
295
  savingLinear.value = true
321
296
  try {
322
297
  await store.save(props.accountId, { secrets: { linearOAuth: null } })
323
- toast.add({
324
- title: t('layout.accountDeployment.linear.cleared'),
325
- icon: 'i-lucide-check',
326
- color: 'success',
327
- })
298
+ actionToast.success('layout.accountDeployment.linear.cleared')
328
299
  } catch (e) {
329
300
  present(e, 'layout.accountDeployment.linear.clearFailed')
330
301
  } finally {
@@ -334,7 +305,7 @@ async function clearLinear() {
334
305
 
335
306
  async function saveFigma() {
336
307
  if (!figma.clientId.trim() || !figma.clientSecret.trim() || !figma.redirectUrl.trim()) {
337
- toast.add({ title: t('layout.accountDeployment.figma.validation'), color: 'error' })
308
+ actionToast.error('layout.accountDeployment.figma.validation')
338
309
  return
339
310
  }
340
311
  savingFigma.value = true
@@ -351,11 +322,7 @@ async function saveFigma() {
351
322
  figma.clientId = ''
352
323
  figma.clientSecret = ''
353
324
  figma.redirectUrl = ''
354
- toast.add({
355
- title: t('layout.accountDeployment.figma.saved'),
356
- icon: 'i-lucide-check',
357
- color: 'success',
358
- })
325
+ actionToast.success('layout.accountDeployment.figma.saved')
359
326
  } catch (e) {
360
327
  present(e, 'layout.accountDeployment.figma.saveFailed')
361
328
  } finally {
@@ -368,11 +335,7 @@ async function clearFigma() {
368
335
  savingFigma.value = true
369
336
  try {
370
337
  await store.save(props.accountId, { secrets: { figmaOAuth: null } })
371
- toast.add({
372
- title: t('layout.accountDeployment.figma.cleared'),
373
- icon: 'i-lucide-check',
374
- color: 'success',
375
- })
338
+ actionToast.success('layout.accountDeployment.figma.cleared')
376
339
  } catch (e) {
377
340
  present(e, 'layout.accountDeployment.figma.clearFailed')
378
341
  } finally {
@@ -384,7 +347,7 @@ async function saveWeb() {
384
347
  const brave = web.braveApiKey.trim()
385
348
  const searxng = web.searxngUrl.trim()
386
349
  if (!brave && !searxng) {
387
- toast.add({ title: t('layout.accountDeployment.web.validation'), color: 'error' })
350
+ actionToast.error('layout.accountDeployment.web.validation')
388
351
  return
389
352
  }
390
353
  savingWeb.value = true
@@ -401,11 +364,7 @@ async function saveWeb() {
401
364
  web.braveApiKey = ''
402
365
  web.searxngUrl = ''
403
366
  web.searxngApiKey = ''
404
- toast.add({
405
- title: t('layout.accountDeployment.web.saved'),
406
- icon: 'i-lucide-check',
407
- color: 'success',
408
- })
367
+ actionToast.success('layout.accountDeployment.web.saved')
409
368
  } catch (e) {
410
369
  present(e, 'layout.accountDeployment.web.saveFailed')
411
370
  } finally {
@@ -418,11 +377,7 @@ async function clearWeb() {
418
377
  savingWeb.value = true
419
378
  try {
420
379
  await store.save(props.accountId, { secrets: { webSearch: null } })
421
- toast.add({
422
- title: t('layout.accountDeployment.web.cleared'),
423
- icon: 'i-lucide-check',
424
- color: 'success',
425
- })
380
+ actionToast.success('layout.accountDeployment.web.cleared')
426
381
  } catch (e) {
427
382
  present(e, 'layout.accountDeployment.web.clearFailed')
428
383
  } finally {
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
+ import EmptyState from '~/components/common/EmptyState.vue'
3
4
  import type { AgentFailureKind, PlatformFailureKindRule } from '~/types/execution'
4
5
  import {
5
6
  AGENT_FAILURE_KINDS,
@@ -165,13 +166,13 @@ const faults = computed(() => failureKindRuleFaults(rules.value))
165
166
  </div>
166
167
 
167
168
  <template v-if="overriding">
168
- <p
169
+ <EmptyState
169
170
  v-if="rules.length === 0"
170
- class="text-2xs text-dimmed"
171
+ compact
172
+ icon="i-lucide-list-filter"
173
+ :title="t('settings.platformAlerts.failureKinds.empty')"
171
174
  data-testid="platform-alert-failure-kinds-empty"
172
- >
173
- {{ t('settings.platformAlerts.failureKinds.empty') }}
174
- </p>
175
+ />
175
176
 
176
177
  <div
177
178
  v-for="(rule, index) in rules"
@@ -16,7 +16,7 @@ import type {
16
16
  const props = defineProps<{ accountId: string }>()
17
17
 
18
18
  const store = useAccountSettingsStore()
19
- const toast = useToast()
19
+ const actionToast = useActionToast()
20
20
  const { present } = usePipelineErrorToast()
21
21
  const { t } = useI18n()
22
22
 
@@ -132,11 +132,7 @@ async function save() {
132
132
  try {
133
133
  // `config` fully replaces the stored non-secret config, so carry the rest forward.
134
134
  await store.save(props.accountId, { config: { ...store.view?.config, modelPolicy: policy } })
135
- toast.add({
136
- title: t('settings.modelPolicy.saved'),
137
- icon: 'i-lucide-check',
138
- color: 'success',
139
- })
135
+ actionToast.success('settings.modelPolicy.saved')
140
136
  } catch (e) {
141
137
  present(e, 'settings.modelPolicy.saveFailed')
142
138
  } finally {
@@ -25,7 +25,7 @@ import {
25
25
  const props = defineProps<{ accountId: string }>()
26
26
 
27
27
  const store = useAccountSettingsStore()
28
- const toast = useToast()
28
+ const actionToast = useActionToast()
29
29
  const { present } = usePipelineErrorToast()
30
30
  const { t } = useI18n()
31
31
 
@@ -187,14 +187,12 @@ const ruleFaults = computed(() => failureKindRuleFaults(failureKindRules.value ?
187
187
 
188
188
  async function save() {
189
189
  if (!loaded.value) {
190
- toast.add({ title: t('settings.platformAlerts.notLoaded'), color: 'error' })
190
+ actionToast.error('settings.platformAlerts.notLoaded')
191
191
  return
192
192
  }
193
193
  if (invalidFields.value.length > 0) {
194
- toast.add({
195
- title: t('settings.platformAlerts.invalidNumbers'),
194
+ actionToast.error('settings.platformAlerts.invalidNumbers', {
196
195
  description: invalidFields.value.join(', '),
197
- color: 'error',
198
196
  })
199
197
  return
200
198
  }
@@ -203,14 +201,12 @@ async function save() {
203
201
  // two faults describe themselves differently because they are fixed differently — a row
204
202
  // number is useless advice for a list that is simply too long.
205
203
  if (hasFailureKindRuleFaults(ruleFaults.value)) {
206
- toast.add({
207
- title: t('settings.platformAlerts.failureKinds.invalidTitle'),
204
+ actionToast.error('settings.platformAlerts.failureKinds.invalidTitle', {
208
205
  description: ruleFaults.value.tooMany
209
206
  ? t('settings.platformAlerts.failureKinds.tooManyRules', { max: MAX_FAILURE_KIND_RULES })
210
207
  : t('settings.platformAlerts.failureKinds.invalidRows', {
211
208
  rows: ruleFaults.value.rows.join(', '),
212
209
  }),
213
- color: 'error',
214
210
  })
215
211
  return
216
212
  }
@@ -229,11 +225,7 @@ async function save() {
229
225
  await store.save(props.accountId, {
230
226
  config: { ...store.view?.config, platformAlerts: settings },
231
227
  })
232
- toast.add({
233
- title: t('settings.platformAlerts.saved'),
234
- icon: 'i-lucide-check',
235
- color: 'success',
236
- })
228
+ actionToast.success('settings.platformAlerts.saved')
237
229
  } catch (e) {
238
230
  present(e, 'settings.platformAlerts.saveFailed')
239
231
  } finally {
@@ -14,12 +14,13 @@ import type {
14
14
  } from '~/types/merge'
15
15
  import RiskPolicyCreateForm from '~/components/settings/RiskPolicyCreateForm.vue'
16
16
  import RiskPolicyEditorRow from '~/components/settings/RiskPolicyEditorRow.vue'
17
+ import EmptyState from '~/components/common/EmptyState.vue'
17
18
 
18
19
  const props = defineProps<{ accountId: string }>()
19
20
 
20
21
  const { t } = useI18n()
21
22
  const store = useAccountRiskPoliciesStore()
22
- const toast = useToast()
23
+ const actionToast = useActionToast()
23
24
  const { present } = usePipelineErrorToast()
24
25
  const { confirm } = useConfirm()
25
26
 
@@ -47,11 +48,7 @@ async function save(policy: RiskPolicyLibraryEntry, patch: UpdateRiskPolicyInput
47
48
  busy.value = policy.id
48
49
  try {
49
50
  await store.update(props.accountId, policy.id, patch)
50
- toast.add({
51
- title: t('settings.riskPolicy.toast.saved'),
52
- icon: 'i-lucide-check',
53
- color: 'success',
54
- })
51
+ actionToast.success('settings.riskPolicy.toast.saved')
55
52
  } catch (e) {
56
53
  present(e, 'settings.riskPolicy.toast.saveFailed')
57
54
  } finally {
@@ -84,11 +81,7 @@ async function create(input: CreateRiskPolicyInput) {
84
81
  creating.value = true
85
82
  try {
86
83
  await store.create(props.accountId, input)
87
- toast.add({
88
- title: t('settings.riskPolicy.toast.created'),
89
- icon: 'i-lucide-check',
90
- color: 'success',
91
- })
84
+ actionToast.success('settings.riskPolicy.toast.created')
92
85
  } catch (e) {
93
86
  present(e, 'settings.riskPolicy.toast.createFailed')
94
87
  } finally {
@@ -101,9 +94,15 @@ async function create(input: CreateRiskPolicyInput) {
101
94
  <div class="space-y-4 text-sm" data-testid="account-risk-policy-panel">
102
95
  <p class="text-2xs text-muted">{{ t('layout.accountRiskPolicies.intro') }}</p>
103
96
 
104
- <p v-if="!store.loading && policies.length === 0" class="text-2xs text-dimmed">
105
- {{ t('layout.accountRiskPolicies.empty') }}
106
- </p>
97
+ <div v-if="store.loading && policies.length === 0" class="space-y-2">
98
+ <USkeleton v-for="i in 2" :key="i" class="h-20 w-full" />
99
+ </div>
100
+ <EmptyState
101
+ v-else-if="policies.length === 0"
102
+ compact
103
+ icon="i-lucide-shield"
104
+ :title="t('layout.accountRiskPolicies.empty')"
105
+ />
107
106
 
108
107
  <RiskPolicyEditorRow
109
108
  v-for="policy in policies"
@@ -16,7 +16,7 @@ import { onMounted, ref, watch } from 'vue'
16
16
  const props = defineProps<{ accountId: string }>()
17
17
 
18
18
  const store = useAccountSettingsStore()
19
- const toast = useToast()
19
+ const actionToast = useActionToast()
20
20
  const { present } = usePipelineErrorToast()
21
21
  const { t } = useI18n()
22
22
 
@@ -48,7 +48,7 @@ async function save() {
48
48
  ...(forbid.value ? { allowInitiatorPat: false } : { allowInitiatorPat: undefined }),
49
49
  },
50
50
  })
51
- toast.add({ title: t('settings.runCredentialPolicy.saved'), color: 'success' })
51
+ actionToast.success('settings.runCredentialPolicy.saved')
52
52
  } catch (e) {
53
53
  present(e, 'settings.runCredentialPolicy.saveFailed')
54
54
  } finally {