@cat-factory/app 0.311.1 → 0.311.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +70 -5
  2. package/app/app.config.ts +6 -18
  3. package/app/components/assistant/AssistantModal.vue +2 -5
  4. package/app/components/auth/AuthGate.vue +2 -1
  5. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +14 -15
  6. package/app/components/binaryOutput/StoredAssetView.vue +1 -3
  7. package/app/components/board/AddTaskModal.vue +3 -10
  8. package/app/components/board/AgentFailureCard.vue +4 -5
  9. package/app/components/board/AgentStopButton.vue +5 -7
  10. package/app/components/board/BoardCanvas.vue +31 -30
  11. package/app/components/board/CreateInitiativeModal.vue +2 -4
  12. package/app/components/board/RecurringPipelineModal.vue +2 -7
  13. package/app/components/board/nodes/AgentChip.vue +3 -2
  14. package/app/components/board/nodes/BlockNode.vue +14 -11
  15. package/app/components/board/nodes/InitiativeCard.vue +6 -3
  16. package/app/components/bootstrap/AdoptionReviewModal.vue +4 -7
  17. package/app/components/bootstrap/BootstrapModal.vue +6 -15
  18. package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
  19. package/app/components/brainstorm/BrainstormWindow.vue +32 -26
  20. package/app/components/bugFishing/BugFishingWindow.vue +15 -13
  21. package/app/components/clarity/ClarityReviewWindow.vue +31 -26
  22. package/app/components/common/AsyncViewError.vue +1 -1
  23. package/app/components/common/ClarificationItem.vue +3 -1
  24. package/app/components/common/EmptyState.vue +41 -19
  25. package/app/components/common/InterviewGateNotice.vue +3 -1
  26. package/app/components/common/Spinner.vue +28 -0
  27. package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
  28. package/app/components/docs/DocInterviewWindow.vue +6 -6
  29. package/app/components/documents/ContextDocumentPicker.vue +2 -2
  30. package/app/components/documents/DocumentImportModal.vue +9 -8
  31. package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
  32. package/app/components/documents/DocumentTemplatesModal.vue +15 -9
  33. package/app/components/documents/RepoContextDocPicker.vue +2 -2
  34. package/app/components/documents/SpawnPreviewModal.vue +4 -6
  35. package/app/components/documents/StartFromDesignModal.vue +2 -1
  36. package/app/components/documents/TaskContextDocs.vue +4 -5
  37. package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
  38. package/app/components/environments/steps/EnvPickStep.vue +7 -3
  39. package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
  40. package/app/components/focus/BlockFocusView.vue +6 -5
  41. package/app/components/followUp/FollowUpWindow.vue +7 -9
  42. package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
  43. package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
  44. package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
  45. package/app/components/foundational/FoundationalServiceSources.vue +20 -22
  46. package/app/components/foundational/FoundationalSuppressions.vue +2 -2
  47. package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
  48. package/app/components/fragments/FragmentLibraryManager.vue +36 -34
  49. package/app/components/fragments/FragmentSelector.vue +7 -3
  50. package/app/components/gates/GateResultView.vue +6 -9
  51. package/app/components/github/AddServiceFromRepoModal.vue +6 -10
  52. package/app/components/github/GitHubConnect.vue +10 -17
  53. package/app/components/github/GitHubPanel.vue +44 -47
  54. package/app/components/github/RepoTreeBrowser.vue +9 -5
  55. package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
  56. package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
  57. package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
  58. package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
  59. package/app/components/humanTest/HumanTestWindow.vue +2 -1
  60. package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
  61. package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
  62. package/app/components/judge/JudgeResultView.vue +2 -7
  63. package/app/components/kaizen/KaizenPanel.vue +20 -5
  64. package/app/components/kaizen/KaizenStepStatus.vue +2 -1
  65. package/app/components/layout/AccountAuditLog.vue +11 -3
  66. package/app/components/layout/AccountDeploymentSettings.vue +17 -62
  67. package/app/components/layout/AccountFailureKindRules.vue +6 -5
  68. package/app/components/layout/AccountModelPolicySettings.vue +2 -6
  69. package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
  70. package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
  71. package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
  72. package/app/components/layout/AccountTeamSettings.vue +12 -9
  73. package/app/components/layout/BoardSwitcher.vue +2 -2
  74. package/app/components/layout/BoardToolbar.vue +3 -11
  75. package/app/components/layout/CommandBar.vue +7 -3
  76. package/app/components/layout/ConnectionStatusBanner.vue +2 -1
  77. package/app/components/layout/IntegrationsHub.vue +7 -3
  78. package/app/components/layout/ModelProvidersHub.vue +7 -3
  79. package/app/components/layout/NotificationsInbox.vue +3 -11
  80. package/app/components/layout/SideBar.vue +1 -1
  81. package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
  82. package/app/components/media/ArtifactLightbox.vue +4 -3
  83. package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
  84. package/app/components/observability/ToolCallList.vue +12 -17
  85. package/app/components/panels/AgentStepDetail.vue +5 -5
  86. package/app/components/panels/GenericStructuredResultView.vue +6 -9
  87. package/app/components/panels/InspectorPanel.vue +2 -1
  88. package/app/components/panels/MergerResultView.vue +5 -6
  89. package/app/components/panels/ObservabilityPanel.vue +26 -41
  90. package/app/components/panels/OperatorDashboardPanel.vue +29 -17
  91. package/app/components/panels/ReportsPanel.vue +17 -11
  92. package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
  93. package/app/components/panels/ResultWindowShell.vue +1 -1
  94. package/app/components/panels/RunDetailLoadState.vue +4 -3
  95. package/app/components/panels/StepContainerStatus.vue +4 -3
  96. package/app/components/panels/StepDelegatedStatus.vue +3 -2
  97. package/app/components/panels/StepMetadataCard.vue +6 -8
  98. package/app/components/panels/StepRunMeta.vue +2 -5
  99. package/app/components/panels/inspector/ContainerSummary.vue +7 -1
  100. package/app/components/panels/inspector/EpicChildren.vue +7 -3
  101. package/app/components/panels/inspector/FrontendConfig.vue +7 -3
  102. package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
  103. package/app/components/panels/inspector/ServiceConnections.vue +7 -3
  104. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
  105. package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
  106. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
  107. package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
  108. package/app/components/panels/inspector/TaskExecution.vue +5 -6
  109. package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
  110. package/app/components/panels/inspector/TaskStructure.vue +0 -1
  111. package/app/components/pipeline/AgentPromptEditor.vue +25 -18
  112. package/app/components/pipeline/PipelineBuilder.vue +14 -24
  113. package/app/components/pipeline/PipelineProgress.vue +16 -17
  114. package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
  115. package/app/components/prReview/PrReviewWindow.vue +16 -20
  116. package/app/components/providers/ApiKeysSection.vue +2 -6
  117. package/app/components/providers/PersonalCredentialModal.vue +2 -6
  118. package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
  119. package/app/components/providers/VendorCredentialsModal.vue +2 -6
  120. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
  121. package/app/components/ralph/RalphLoopResultView.vue +2 -7
  122. package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
  123. package/app/components/sandbox/SandboxPanel.vue +37 -25
  124. package/app/components/settings/AccountSettingsPanel.vue +6 -3
  125. package/app/components/settings/ApiTokensPanel.vue +2 -6
  126. package/app/components/settings/BudgetSettings.vue +2 -2
  127. package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
  128. package/app/components/settings/CloudflareHandlerSection.vue +3 -7
  129. package/app/components/settings/ConsensusGroupsSection.vue +7 -3
  130. package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
  131. package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
  132. package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
  133. package/app/components/settings/IssueTrackerPanel.vue +9 -9
  134. package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
  135. package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
  136. package/app/components/settings/McpAuthorizeScreen.vue +8 -3
  137. package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
  138. package/app/components/settings/ModelConfigurationPanel.vue +45 -17
  139. package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
  140. package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
  141. package/app/components/settings/PackageRegistriesPanel.vue +2 -6
  142. package/app/components/settings/ProviderConnectionTab.vue +3 -7
  143. package/app/components/settings/RiskPolicyPanel.vue +4 -16
  144. package/app/components/settings/SharedStacksPanel.vue +8 -18
  145. package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
  146. package/app/components/settings/UsageSettings.vue +11 -2
  147. package/app/components/settings/UserSecretsSection.vue +5 -7
  148. package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
  149. package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
  150. package/app/components/skills/ReviewSkillQueue.vue +7 -3
  151. package/app/components/skills/SkillLibraryManager.vue +20 -18
  152. package/app/components/slack/SlackPanel.vue +5 -8
  153. package/app/components/spec/ServiceSpecWindow.vue +32 -29
  154. package/app/components/tasks/BugHuntModal.vue +23 -19
  155. package/app/components/tasks/ContextIssuePicker.vue +2 -2
  156. package/app/components/tasks/TaskContextIssues.vue +4 -5
  157. package/app/components/tasks/TaskImportModal.vue +10 -8
  158. package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
  159. package/app/components/testing/TestReportWindow.vue +6 -12
  160. package/app/components/tutorial/TutorialCatalogue.vue +7 -3
  161. package/app/components/tutorial/TutorialOverlay.vue +2 -1
  162. package/app/components/tutorial/TutorialPrompt.vue +7 -3
  163. package/app/components/vcs/GitLabConnect.vue +2 -6
  164. package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
  165. package/app/composables/toastTitleKeys.spec.ts +143 -0
  166. package/app/composables/useActionToast.spec.ts +71 -0
  167. package/app/composables/useActionToast.ts +101 -0
  168. package/app/composables/useBlockDeletion.ts +3 -6
  169. package/app/composables/useConfirmAction.ts +5 -3
  170. package/app/composables/useContextLinking.spec.ts +3 -0
  171. package/app/composables/useContextLinking.ts +11 -16
  172. package/app/composables/useCopyToClipboard.ts +4 -3
  173. package/app/composables/useNavContributions.ts +4 -5
  174. package/app/composables/usePipelineErrorToast.spec.ts +24 -0
  175. package/app/composables/usePipelineErrorToast.ts +26 -2
  176. package/app/composables/usePipelineLibraryActions.ts +8 -9
  177. package/app/pages/index.vue +3 -2
  178. package/app/stores/board/context.ts +4 -3
  179. package/app/stores/board/placement.ts +8 -20
  180. package/app/stores/board/removal.ts +4 -13
  181. package/app/stores/board.spec.ts +10 -1
  182. package/app/stores/board.ts +3 -2
  183. package/app/utils/bootstrapSteps.ts +11 -2
  184. package/app/utils/pipelineRender.ts +21 -8
  185. package/i18n/locales/de.json +12 -30
  186. package/i18n/locales/en.json +12 -30
  187. package/i18n/locales/es.json +12 -30
  188. package/i18n/locales/fr.json +12 -30
  189. package/i18n/locales/he.json +12 -30
  190. package/i18n/locales/it.json +12 -30
  191. package/i18n/locales/ja.json +12 -30
  192. package/i18n/locales/pl.json +12 -30
  193. package/i18n/locales/tr.json +12 -30
  194. package/i18n/locales/uk.json +12 -30
  195. package/package.json +1 -1
@@ -3,6 +3,7 @@ import type { DocumentSourceKind } from '~/types/domain'
3
3
  import DocumentSyncState from '~/components/documents/DocumentSyncState.vue'
4
4
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
5
5
  import SectionLabel from '~/components/common/SectionLabel.vue'
6
+ import EmptyState from '~/components/common/EmptyState.vue'
6
7
 
7
8
  // Import pages from a connected document source and pick one to expand into
8
9
  // board structure. A source selector lets the user choose which connected source
@@ -11,7 +12,7 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
11
12
  const { t } = useI18n()
12
13
  const ui = useUiStore()
13
14
  const documents = useDocumentsStore()
14
- const toast = useToast()
15
+ const actionToast = useActionToast()
15
16
  const { present } = usePipelineErrorToast()
16
17
 
17
18
  const open = computed({
@@ -58,10 +59,7 @@ async function doImport() {
58
59
  try {
59
60
  const doc = await documents.importDocument(source.value, value)
60
61
  ref_.value = ''
61
- toast.add({
62
- title: t('documents.import.imported', { title: doc.title }),
63
- icon: 'i-lucide-file-down',
64
- })
62
+ actionToast.success('documents.import.imported', { params: { title: doc.title } })
65
63
  } catch (e) {
66
64
  present(e, 'documents.import.importFailed')
67
65
  } finally {
@@ -157,9 +155,12 @@ function preview(externalId: string) {
157
155
  </div>
158
156
  </div>
159
157
  </div>
160
- <p v-else class="text-center text-xs text-dimmed">
161
- {{ t('documents.import.noneImported') }}
162
- </p>
158
+ <EmptyState
159
+ v-else
160
+ compact
161
+ icon="i-lucide-file-down"
162
+ :title="t('documents.import.noneImported')"
163
+ />
163
164
  </div>
164
165
  </template>
165
166
  </UModal>
@@ -17,7 +17,7 @@ import SecretInput from '~/components/common/SecretInput.vue'
17
17
  const { t } = useI18n()
18
18
  const ui = useUiStore()
19
19
  const documents = useDocumentsStore()
20
- const toast = useToast()
20
+ const actionToast = useActionToast()
21
21
  const { present } = usePipelineErrorToast()
22
22
  const { confirmAction } = useConfirmAction()
23
23
 
@@ -82,10 +82,8 @@ async function submit() {
82
82
  saving.value = true
83
83
  try {
84
84
  await documents.connect(source.value, credentials)
85
- toast.add({
86
- title: t('documents.connect.connected', { source: descriptor.value!.label }),
87
- icon: 'i-lucide-check',
88
- color: 'success',
85
+ actionToast.success('documents.connect.connected', {
86
+ params: { source: descriptor.value!.label },
89
87
  })
90
88
  ui.closeDocumentConnect()
91
89
  } catch (e) {
@@ -99,11 +97,13 @@ async function disconnect() {
99
97
  if (!source.value) return
100
98
  const label = descriptor.value?.label ?? t('documents.connect.sourceFallback')
101
99
  if (!(await confirmAction('disconnect', label))) return
102
- await documents.disconnect(source.value)
103
- toast.add({
104
- title: t('documents.connect.disconnected', { source: label }),
105
- icon: 'i-lucide-unplug',
106
- })
100
+ try {
101
+ await documents.disconnect(source.value)
102
+ } catch (e) {
103
+ present(e, 'documents.connect.disconnectFailed')
104
+ return
105
+ }
106
+ actionToast.success('documents.connect.disconnected', { params: { source: label } })
107
107
  ui.closeDocumentConnect()
108
108
  }
109
109
  </script>
@@ -4,6 +4,7 @@ import type { DocKind, DocumentLinkRole, SourceDocument } from '~/types/domain'
4
4
  import DocumentOriginLink from '~/components/documents/DocumentOriginLink.vue'
5
5
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
6
6
  import SectionLabel from '~/components/common/SectionLabel.vue'
7
+ import EmptyState from '~/components/common/EmptyState.vue'
7
8
 
8
9
  // Manage the workspace's per-DocKind TEMPLATE (singular) + EXEMPLAR (multi) document links (WS1).
9
10
  // A kind can be pointed at one of the workspace's already-imported documents so its parsed
@@ -97,13 +98,15 @@ async function unlink(doc: SourceDocument) {
97
98
  <p class="text-xs text-muted">{{ t('documents.templates.intro') }}</p>
98
99
 
99
100
  <!-- No imported documents yet: a template/exemplar must be an imported document. -->
100
- <div v-if="!documents.documents.length" class="space-y-3 text-center">
101
- <UIcon name="i-lucide-file-plus" class="mx-auto h-8 w-8 text-dimmed" />
102
- <p class="text-sm text-muted">{{ t('documents.templates.importFirst') }}</p>
101
+ <EmptyState
102
+ v-if="!documents.documents.length"
103
+ icon="i-lucide-file-plus"
104
+ :title="t('documents.templates.importFirst')"
105
+ >
103
106
  <UButton variant="soft" icon="i-lucide-file-down" @click="ui.openDocumentImport()">
104
107
  {{ t('documents.templates.importButton') }}
105
108
  </UButton>
106
- </div>
109
+ </EmptyState>
107
110
 
108
111
  <template v-else>
109
112
  <UFormField :label="t('documents.templates.kindLabel')">
@@ -136,7 +139,7 @@ async function unlink(doc: SourceDocument) {
136
139
  </p>
137
140
  <div
138
141
  v-if="template"
139
- class="mt-2 flex items-center justify-between gap-2 rounded-md bg-default/70 px-3 py-2"
142
+ class="mt-2 flex items-center justify-between gap-2 rounded-md bg-elevated/70 px-3 py-2"
140
143
  >
141
144
  <DocumentOriginLink
142
145
  :url="template.url"
@@ -173,7 +176,7 @@ async function unlink(doc: SourceDocument) {
173
176
  <div
174
177
  v-for="doc in exemplars"
175
178
  :key="`${doc.source}:${doc.externalId}`"
176
- class="flex items-center justify-between gap-2 rounded-md bg-default/70 px-3 py-2"
179
+ class="flex items-center justify-between gap-2 rounded-md bg-elevated/70 px-3 py-2"
177
180
  >
178
181
  <DocumentOriginLink
179
182
  :url="doc.url"
@@ -194,9 +197,12 @@ async function unlink(doc: SourceDocument) {
194
197
  </UButton>
195
198
  </div>
196
199
  </div>
197
- <p v-else class="mt-2 text-xs text-dimmed">
198
- {{ t('documents.templates.exemplarsEmpty') }}
199
- </p>
200
+ <EmptyState
201
+ v-else
202
+ compact
203
+ icon="i-lucide-files"
204
+ :title="t('documents.templates.exemplarsEmpty')"
205
+ />
200
206
  </section>
201
207
 
202
208
  <!-- Picker: choose an imported document, then set as template or add as example. -->
@@ -232,8 +232,8 @@ onMounted(() => {
232
232
  <div v-if="fileMode === 'search'" class="space-y-2">
233
233
  <UInput
234
234
  v-model="fileQuery"
235
- :icon="loadingFiles ? 'i-lucide-loader-circle' : 'i-lucide-file-search'"
236
- :ui="{ leadingIcon: loadingFiles ? 'animate-spin' : '' }"
235
+ icon="i-lucide-file-search"
236
+ :loading="loadingFiles"
237
237
  size="sm"
238
238
  class="w-full"
239
239
  :placeholder="t('documents.repoPicker.searchFilesPlaceholder')"
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { isDesignSource } from '@cat-factory/contracts'
3
3
  import type { DocumentBoardPlan } from '~/types/domain'
4
+ import Spinner from '~/components/common/Spinner.vue'
4
5
 
5
6
  // Preview the structure an imported document expands into, then spawn it. The
6
7
  // plan is fetched fresh on open; a badge makes clear whether an LLM or the
@@ -16,7 +17,7 @@ const { t } = useI18n()
16
17
  const ui = useUiStore()
17
18
  const board = useBoardStore()
18
19
  const documents = useDocumentsStore()
19
- const toast = useToast()
20
+ const actionToast = useActionToast()
20
21
  const { present } = usePipelineErrorToast()
21
22
 
22
23
  /** Design documents are planned into an existing service; prose documents at the board root. */
@@ -102,16 +103,13 @@ async function spawn() {
102
103
  modules: t('documents.spawn.moduleCount', { count: result.modules }, result.modules),
103
104
  tasks: t('documents.spawn.taskCount', { count: result.tasks }, result.tasks),
104
105
  })
105
- toast.add({
106
- title: t('documents.spawn.spawned'),
106
+ actionToast.success('documents.spawn.spawned', {
107
107
  // A reused module is stated only when there was one, and never folded into the created
108
108
  // count: a spawn into a service that already had every planned module otherwise reports
109
109
  // "0 modules" beside its tasks, which reads as the modules having failed.
110
110
  description: result.reusedModules
111
111
  ? `${summary} · ${t('documents.spawn.reusedCount', { count: result.reusedModules }, result.reusedModules)}`
112
112
  : summary,
113
- icon: 'i-lucide-check',
114
- color: 'success',
115
113
  })
116
114
  ui.closeSpawnPreview()
117
115
  ui.closeDocumentImport()
@@ -168,7 +166,7 @@ async function spawn() {
168
166
  </p>
169
167
 
170
168
  <div v-if="loadingPlan" class="flex items-center gap-2 text-sm text-muted">
171
- <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
169
+ <Spinner class="h-4 w-4" />
172
170
  {{ t('documents.spawn.buildingPlan') }}
173
171
  </div>
174
172
 
@@ -20,6 +20,7 @@
20
20
  // render identically. For a designer that widening IS the defect, not a detail.
21
21
  import { refRowFor, classifyRefFailure, type RefState } from './ContextDocumentPicker.logic'
22
22
  import type { DocumentSourceKind, ResolvedDocumentRef } from '~/types/domain'
23
+ import Spinner from '~/components/common/Spinner.vue'
23
24
 
24
25
  const { t } = useI18n()
25
26
  const ui = useUiStore()
@@ -178,7 +179,7 @@ function stage() {
178
179
  v-if="state.status === 'checking'"
179
180
  class="flex items-center gap-2 text-sm text-muted"
180
181
  >
181
- <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
182
+ <Spinner class="h-4 w-4" />
182
183
  {{ t('documents.startFromDesign.checking') }}
183
184
  </div>
184
185
 
@@ -6,6 +6,7 @@ import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.
6
6
  import DocumentOriginLink from '~/components/documents/DocumentOriginLink.vue'
7
7
  import DocumentSyncState from '~/components/documents/DocumentSyncState.vue'
8
8
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
9
+ import EmptyState from '~/components/common/EmptyState.vue'
9
10
 
10
11
  // Documents (from any source) attached to a task OR an initiative as agent
11
12
  // context, shown inside the InspectorPanel. An initiative takes the same
@@ -26,7 +27,7 @@ const props = defineProps<{ block: Block }>()
26
27
  const { t } = useI18n()
27
28
  const documents = useDocumentsStore()
28
29
  const ui = useUiStore()
29
- const toast = useToast()
30
+ const actionToast = useActionToast()
30
31
  const { linkPending, presentLinkFailures } = useContextLinking()
31
32
 
32
33
  onMounted(() => {
@@ -88,7 +89,7 @@ async function attach(item: PendingContext) {
88
89
  try {
89
90
  const failures = await linkPending(props.block.id, [item])
90
91
  if (failures.length) presentLinkFailures(failures, props.block.id)
91
- else toast.add({ title: t('documents.taskDocs.attached'), icon: 'i-lucide-link' })
92
+ else actionToast.success('documents.taskDocs.attached')
92
93
  } finally {
93
94
  linking.value = false
94
95
  }
@@ -165,8 +166,6 @@ async function attach(item: PendingContext) {
165
166
  <DocumentSyncState :doc="doc" class="mt-1" />
166
167
  </div>
167
168
  </div>
168
- <p v-else class="text-2xs text-dimmed">
169
- {{ emptyHint }}
170
- </p>
169
+ <EmptyState v-else compact icon="i-lucide-file-text" :title="emptyHint" />
171
170
  </InspectorSection>
172
171
  </template>
@@ -10,6 +10,8 @@ import {
10
10
  showsProviderFailure,
11
11
  } from '~/components/environments/EnvironmentStatusPanel.logic'
12
12
  import SectionLabel from '~/components/common/SectionLabel.vue'
13
+ import Spinner from '~/components/common/Spinner.vue'
14
+ import EmptyState from '~/components/common/EmptyState.vue'
13
15
 
14
16
  const props = defineProps<{
15
17
  environment: RunEnvironment | null
@@ -112,13 +114,11 @@ const envInTransition = computed(
112
114
  </SectionLabel>
113
115
  <div v-if="environment" class="space-y-2">
114
116
  <div class="flex items-center gap-2 text-sm">
115
- <UIcon
117
+ <Spinner
116
118
  :name="ENV_STATUS_META[environment.status].icon"
119
+ :spinning="runActive && envInTransition"
117
120
  class="h-3.5 w-3.5"
118
- :class="[
119
- ENV_STATUS_META[environment.status].color,
120
- { 'animate-spin': runActive && envInTransition },
121
- ]"
121
+ :class="ENV_STATUS_META[environment.status].color"
122
122
  />
123
123
  <span :class="ENV_STATUS_META[environment.status].color">{{
124
124
  ENV_STATUS_META[environment.status].label
@@ -166,8 +166,11 @@ const envInTransition = computed(
166
166
  {{ t('environments.statusNote', { note: statusNote }) }}
167
167
  </p>
168
168
  </div>
169
- <p v-else class="text-xs text-dimmed">
170
- {{ degradedReason ?? t('environments.empty') }}
171
- </p>
169
+ <EmptyState
170
+ v-else
171
+ compact
172
+ icon="i-lucide-server"
173
+ :title="degradedReason ?? t('environments.empty')"
174
+ />
172
175
  </section>
173
176
  </template>
@@ -5,6 +5,7 @@
5
5
  // advances to the review step. Rendered by `JourneyOutlet`, so it receives the
6
6
  // `ModuleEntryProps` (`input` / `exit` / `goBack`) as attributes.
7
7
  import type { EnvSelectOutput } from '~/modular/journeys/environmentSetup.logic'
8
+ import EmptyState from '~/components/common/EmptyState.vue'
8
9
 
9
10
  defineProps<{
10
11
  input: { frameId: string | null }
@@ -21,9 +22,12 @@ const { t } = useI18n()
21
22
  <!-- The one-service scope caveat, stated on the step that asks for the service: nothing
22
23
  here changes any other service, and running it twice just re-edits this one's recipe. -->
23
24
  <p class="text-xs leading-relaxed text-dimmed">{{ t('environmentWizard.pick.scope') }}</p>
24
- <p v-if="!store.serviceFrames.length" class="text-sm text-dimmed">
25
- {{ t('environmentWizard.pick.empty') }}
26
- </p>
25
+ <EmptyState
26
+ v-if="!store.serviceFrames.length"
27
+ compact
28
+ icon="i-lucide-box"
29
+ :title="t('environmentWizard.pick.empty')"
30
+ />
27
31
  <div v-else class="space-y-1.5">
28
32
  <UButton
29
33
  v-for="frame in store.serviceFrames"
@@ -6,6 +6,7 @@
6
6
  import { computed } from 'vue'
7
7
  import JourneyStepNav from '~/components/environments/steps/JourneyStepNav.vue'
8
8
  import { useEnvironmentWizardTarget } from '~/modular/journeys/environmentSetup.frame'
9
+ import EmptyState from '~/components/common/EmptyState.vue'
9
10
 
10
11
  const props = defineProps<{
11
12
  input: { frameId: string | null }
@@ -48,13 +49,13 @@ const PREFLIGHT_COLOR: Record<'pass' | 'warn' | 'fail', 'success' | 'warning' |
48
49
  </UButton>
49
50
  </div>
50
51
 
51
- <p
52
+ <EmptyState
52
53
  v-if="!store.recipe.prerequisites?.length"
53
- class="text-xs text-dimmed"
54
+ compact
55
+ icon="i-lucide-list-checks"
56
+ :title="t('environmentWizard.preflight.none')"
54
57
  data-testid="env-setup-preflight-none"
55
- >
56
- {{ t('environmentWizard.preflight.none') }}
57
- </p>
58
+ />
58
59
  <p
59
60
  v-else-if="preflightsUnavailable"
60
61
  class="text-xs text-app-warning-300/80"
@@ -7,6 +7,7 @@ import PipelineProgress from '~/components/pipeline/PipelineProgress.vue'
7
7
  import IconButton from '~/components/common/IconButton.vue'
8
8
  import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
9
9
  import SectionLabel from '~/components/common/SectionLabel.vue'
10
+ import EmptyState from '~/components/common/EmptyState.vue'
10
11
 
11
12
  const board = useBoardStore()
12
13
  const pipelines = usePipelinesStore()
@@ -214,12 +215,12 @@ function openApprovalFor(approvalId: string) {
214
215
  @open-approval="openApprovalFor"
215
216
  />
216
217
 
217
- <div
218
+ <EmptyState
218
219
  v-else
219
- class="flex flex-1 items-center justify-center rounded-xl border border-dashed border-muted text-sm text-dimmed"
220
- >
221
- {{ t('focus.emptyPipelineHint') }}
222
- </div>
220
+ class="flex-1"
221
+ icon="i-lucide-workflow"
222
+ :title="t('focus.emptyPipelineHint')"
223
+ />
223
224
  </section>
224
225
 
225
226
  <!-- side: details -->
@@ -13,6 +13,7 @@ import { useFollowUpsStore } from '~/stores/followUps'
13
13
  import type { FollowUpItem, FollowUpResolution } from '~/types/execution'
14
14
  import { FOLLOW_UP_COMPANION_META } from '~/utils/catalog'
15
15
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
16
+ import EmptyState from '~/components/common/EmptyState.vue'
16
17
 
17
18
  const execution = useExecutionStore()
18
19
  const board = useBoardStore()
@@ -156,16 +157,13 @@ function hasRecordedAnswer(item: FollowUpItem): boolean {
156
157
 
157
158
  <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
158
159
  <!-- Empty -->
159
- <div
160
+ <EmptyState
160
161
  v-if="items.length === 0"
161
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-muted"
162
- >
163
- <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-8 w-8 opacity-40" />
164
- <p class="text-sm">{{ t('followUp.empty.title') }}</p>
165
- <p class="max-w-sm text-2xs text-dimmed">
166
- {{ t('followUp.empty.hint') }}
167
- </p>
168
- </div>
162
+ class="h-full"
163
+ :icon="FOLLOW_UP_COMPANION_META.icon"
164
+ :title="t('followUp.empty.title')"
165
+ :description="t('followUp.empty.hint')"
166
+ />
169
167
 
170
168
  <div v-else class="space-y-3">
171
169
  <p
@@ -15,6 +15,8 @@ import { useForkDecisionStore } from '~/stores/forkDecision'
15
15
  import type { ForkChatMessage, ForkDecisionStepState, ForkOption } from '~/types/execution'
16
16
  import { FORK_DECISION_META } from '~/utils/catalog'
17
17
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
18
+ import Spinner from '~/components/common/Spinner.vue'
19
+ import EmptyState from '~/components/common/EmptyState.vue'
18
20
 
19
21
  const execution = useExecutionStore()
20
22
  const board = useBoardStore()
@@ -170,7 +172,7 @@ const { requestClose } = useUnsavedGuard({
170
172
  v-if="status === 'proposing'"
171
173
  class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-muted"
172
174
  >
173
- <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
175
+ <Spinner class="h-8 w-8 opacity-60" />
174
176
  <p class="text-sm">{{ t('forkDecision.proposing.title') }}</p>
175
177
  <p class="max-w-sm text-2xs text-dimmed">
176
178
  {{ t('forkDecision.proposing.hint') }}
@@ -322,7 +324,7 @@ const { requestClose } = useUnsavedGuard({
322
324
  <p
323
325
  class="flex items-center gap-1.5 rounded-lg bg-elevated/70 px-3 py-1.5 text-xs text-muted"
324
326
  >
325
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
327
+ <Spinner class="h-3.5 w-3.5" />
326
328
  {{ t('forkDecision.chat.thinking') }}
327
329
  </p>
328
330
  </div>
@@ -364,13 +366,12 @@ const { requestClose } = useUnsavedGuard({
364
366
  </div>
365
367
 
366
368
  <!-- Skipped / no state: nothing to decide. -->
367
- <div
369
+ <EmptyState
368
370
  v-else
369
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-muted"
370
- >
371
- <UIcon :name="FORK_DECISION_META.icon" class="h-8 w-8 opacity-40" />
372
- <p class="text-sm">{{ t('forkDecision.empty.title') }}</p>
373
- </div>
371
+ class="h-full"
372
+ :icon="FORK_DECISION_META.icon"
373
+ :title="t('forkDecision.empty.title')"
374
+ />
374
375
  </div>
375
376
 
376
377
  <footer
@@ -20,9 +20,10 @@ import type { ApiContractFormat, FoundationalServiceTier } from '~/types/domain'
20
20
  import { useFoundationalServicesStore } from '~/stores/foundationalServices'
21
21
  import FoundationalContractSummary from '~/components/foundational/FoundationalContractSummary.vue'
22
22
  import IconButton from '~/components/common/IconButton.vue'
23
+ import EmptyState from '~/components/common/EmptyState.vue'
23
24
 
24
25
  const catalog = useFoundationalServicesStore()
25
- const toast = useToast()
26
+ const actionToast = useActionToast()
26
27
  const { present } = usePipelineErrorToast()
27
28
  const { t } = useI18n()
28
29
 
@@ -71,7 +72,7 @@ async function suppress(serviceId: string) {
71
72
  await withRow(`suppress:${serviceId}`, async () => {
72
73
  try {
73
74
  await catalog.suppress(serviceId)
74
- toast.add({ title: t('foundational.toast.suppressed'), icon: 'i-lucide-eye-off' })
75
+ actionToast.success('foundational.toast.suppressed')
75
76
  } catch (e) {
76
77
  present(e, 'foundational.toast.suppressFailed')
77
78
  }
@@ -156,8 +157,11 @@ async function suppress(serviceId: string) {
156
157
  </div>
157
158
  </div>
158
159
 
159
- <p v-if="!catalog.resolved.length" class="text-sm text-dimmed">
160
- {{ t('foundational.catalog.empty') }}
161
- </p>
160
+ <EmptyState
161
+ v-if="!catalog.resolved.length"
162
+ compact
163
+ icon="i-lucide-package"
164
+ :title="t('foundational.catalog.empty')"
165
+ />
162
166
  </div>
163
167
  </template>
@@ -21,6 +21,7 @@ import {
21
21
  } from '~/stores/foundationalServices'
22
22
  import FoundationalContractSummary from '~/components/foundational/FoundationalContractSummary.vue'
23
23
  import IconButton from '~/components/common/IconButton.vue'
24
+ import EmptyState from '~/components/common/EmptyState.vue'
24
25
 
25
26
  const props = defineProps<{ kind: FoundationalServiceOwnerKind; ownerId: string }>()
26
27
 
@@ -28,7 +29,7 @@ const catalog =
28
29
  props.kind === 'workspace'
29
30
  ? useFoundationalServicesStore()
30
31
  : useFoundationalServices(props.kind, props.ownerId)
31
- const toast = useToast()
32
+ const actionToast = useActionToast()
32
33
  const { present } = usePipelineErrorToast()
33
34
  const { t } = useI18n()
34
35
  const { confirm } = useConfirm()
@@ -131,7 +132,7 @@ async function save() {
131
132
  capabilities: capabilityList.value,
132
133
  ...(contracts ? { contracts } : {}),
133
134
  })
134
- toast.add({ title: t('foundational.toast.created'), icon: 'i-lucide-check' })
135
+ actionToast.success('foundational.toast.created')
135
136
  } else {
136
137
  await catalog.update(draft.id, {
137
138
  name: draft.name.trim(),
@@ -140,7 +141,7 @@ async function save() {
140
141
  capabilities: capabilityList.value,
141
142
  ...(contracts ? { contracts } : {}),
142
143
  })
143
- toast.add({ title: t('foundational.toast.updated'), icon: 'i-lucide-check' })
144
+ actionToast.success('foundational.toast.updated')
144
145
  }
145
146
  editing.value = null
146
147
  } catch (e) {
@@ -171,7 +172,7 @@ async function remove(service: FoundationalService) {
171
172
  if (!ok) return
172
173
  try {
173
174
  await catalog.remove(service.id)
174
- toast.add({ title: t('foundational.toast.deleted'), icon: 'i-lucide-trash-2' })
175
+ actionToast.success('foundational.toast.deleted')
175
176
  } catch (e) {
176
177
  present(e, 'foundational.toast.deleteFailed')
177
178
  }
@@ -224,9 +225,12 @@ async function remove(service: FoundationalService) {
224
225
  </div>
225
226
  </div>
226
227
  </div>
227
- <p v-if="!catalog.services.length" class="text-sm text-dimmed">
228
- {{ t('foundational.registry.empty') }}
229
- </p>
228
+ <EmptyState
229
+ v-if="!catalog.services.length"
230
+ compact
231
+ icon="i-lucide-package"
232
+ :title="t('foundational.registry.empty')"
233
+ />
230
234
 
231
235
  <UButton
232
236
  v-if="editing === null"
@@ -28,6 +28,7 @@ import {
28
28
  import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.vue'
29
29
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
30
30
  import IconButton from '~/components/common/IconButton.vue'
31
+ import EmptyState from '~/components/common/EmptyState.vue'
31
32
 
32
33
  const props = defineProps<{ kind: FoundationalServiceOwnerKind; ownerId: string }>()
33
34
 
@@ -36,7 +37,7 @@ const catalog =
36
37
  ? useFoundationalServicesStore()
37
38
  : useFoundationalServices(props.kind, props.ownerId)
38
39
  const github = useGitHubStore()
39
- const toast = useToast()
40
+ const actionToast = useActionToast()
40
41
  const { present } = usePipelineErrorToast()
41
42
  const { t, d } = useI18n()
42
43
  const { confirm } = useConfirm()
@@ -163,7 +164,7 @@ async function link() {
163
164
  : {}),
164
165
  })
165
166
  resetDraft()
166
- toast.add({ title: t('foundational.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
167
+ actionToast.success('foundational.toast.sourceLinked')
167
168
  return result
168
169
  } catch (e) {
169
170
  present(e, 'foundational.toast.linkSourceFailed')
@@ -185,17 +186,14 @@ async function sync(id: string) {
185
186
  )
186
187
  const coverage = result.folderScan ? folderScanNotes.value[result.folderScan] : null
187
188
  if (coverage) notes.push(coverage)
188
- toast.add({
189
- title: t('foundational.toast.synced', {
190
- updated: result.upserted,
191
- removed: result.tombstoned,
192
- }),
189
+ // A folder we could not fully see, or could not find at all, is the one sync outcome a
190
+ // human has to act on: the counts alone would read as an ordinary quiet success.
191
+ const synced = {
192
+ params: { updated: result.upserted, removed: result.tombstoned },
193
193
  ...(notes.length ? { description: notes.join(' ') } : {}),
194
- icon: 'i-lucide-refresh-cw',
195
- // A folder we could not fully see, or could not find at all, is the one sync outcome a
196
- // human has to act on — the counts alone would read as an ordinary quiet success.
197
- color: coverage ? 'warning' : 'info',
198
- })
194
+ }
195
+ if (coverage) actionToast.warning('foundational.toast.synced', synced)
196
+ else actionToast.success('foundational.toast.synced', synced)
199
197
  } catch (e) {
200
198
  present(e, 'foundational.toast.syncFailed')
201
199
  }
@@ -206,12 +204,9 @@ async function check(id: string) {
206
204
  await withRow(`check:${id}`, async () => {
207
205
  try {
208
206
  const status = await catalog.checkSource(id)
209
- toast.add({
210
- title: status.changed
211
- ? t('foundational.toast.changesAvailable')
212
- : t('foundational.toast.upToDate'),
213
- icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
214
- })
207
+ actionToast.info(
208
+ status.changed ? 'foundational.toast.changesAvailable' : 'foundational.toast.upToDate',
209
+ )
215
210
  } catch (e) {
216
211
  present(e, 'foundational.toast.checkSourceFailed')
217
212
  }
@@ -233,7 +228,7 @@ async function unlink(id: string) {
233
228
  await withRow(`unlink:${id}`, async () => {
234
229
  try {
235
230
  await catalog.unlinkSource(id)
236
- toast.add({ title: t('foundational.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
231
+ actionToast.success('foundational.toast.sourceUnlinked')
237
232
  } catch (e) {
238
233
  present(e, 'foundational.toast.unlinkSourceFailed')
239
234
  }
@@ -325,9 +320,12 @@ async function unlink(id: string) {
325
320
  />
326
321
  </div>
327
322
  </div>
328
- <p v-if="!catalog.sources.length" class="text-sm text-dimmed">
329
- {{ t('foundational.sources.empty') }}
330
- </p>
323
+ <EmptyState
324
+ v-if="!catalog.sources.length"
325
+ compact
326
+ icon="i-lucide-git-branch"
327
+ :title="t('foundational.sources.empty')"
328
+ />
331
329
 
332
330
  <!-- Linking needs the GitHub integration; say so rather than offering a form that 503s. -->
333
331
  <div