@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
@@ -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 {
@@ -9,6 +9,7 @@ import AccountPlatformAlertSettings from '~/components/layout/AccountPlatformAle
9
9
  import AccountRunCredentialSettings from '~/components/layout/AccountRunCredentialSettings.vue'
10
10
  import SecretInput from '~/components/common/SecretInput.vue'
11
11
  import IconButton from '~/components/common/IconButton.vue'
12
+ import EmptyState from '~/components/common/EmptyState.vue'
12
13
 
13
14
  // Team settings for an org account: the member roster (with combinable admin /
14
15
  // developer / product roles), pending email invitations, and the per-account
@@ -19,7 +20,7 @@ const props = defineProps<{ accountId: string }>()
19
20
  const accounts = useAccountsStore()
20
21
  const uiMode = useUiModeStore()
21
22
  const auth = useAuthStore()
22
- const toast = useToast()
23
+ const actionToast = useActionToast()
23
24
  const { present } = usePipelineErrorToast()
24
25
  const { t, te } = useI18n()
25
26
  const { confirmAction, toastDone } = useConfirmAction()
@@ -77,7 +78,7 @@ async function revokeSessions(userId: string, label: string) {
77
78
  if (!(await confirmAction('revoke', label))) return
78
79
  try {
79
80
  await accounts.revokeMemberSessions(props.accountId, userId)
80
- toast.add({ title: t('layout.accountTeam.members.sessionsRevoked'), icon: 'i-lucide-check' })
81
+ actionToast.success('layout.accountTeam.members.sessionsRevoked')
81
82
  } catch (e) {
82
83
  present(e, 'layout.accountTeam.errors.revokeSessions')
83
84
  }
@@ -114,7 +115,7 @@ async function createOrganization() {
114
115
  try {
115
116
  await accounts.createOrg(name)
116
117
  newOrgName.value = ''
117
- toast.add({ title: t('layout.accountTeam.org.created'), icon: 'i-lucide-check' })
118
+ actionToast.success('layout.accountTeam.org.created')
118
119
  } catch (e) {
119
120
  present(e, 'layout.accountTeam.errors.createOrg')
120
121
  } finally {
@@ -136,12 +137,10 @@ async function sendInvite() {
136
137
  inviteRoles.value.length ? inviteRoles.value : ['developer'],
137
138
  )
138
139
  inviteEmail.value = ''
139
- toast.add({
140
- title: t('layout.accountTeam.invite.created'),
140
+ actionToast.success('layout.accountTeam.invite.created', {
141
141
  description: acceptUrl
142
142
  ? t('layout.accountTeam.invite.createdEmailed')
143
143
  : t('layout.accountTeam.invite.createdShareLink'),
144
- icon: 'i-lucide-mail-check',
145
144
  })
146
145
  } catch (e) {
147
146
  present(e, 'layout.accountTeam.errors.sendInvite')
@@ -175,7 +174,7 @@ async function connectEmail() {
175
174
  fromAddress: emailFrom.value.trim(),
176
175
  })
177
176
  emailApiKey.value = ''
178
- toast.add({ title: t('layout.accountTeam.email.connected'), icon: 'i-lucide-check' })
177
+ actionToast.success('layout.accountTeam.email.connected')
179
178
  } catch (e) {
180
179
  present(e, 'layout.accountTeam.errors.connectEmail')
181
180
  } finally {
@@ -261,8 +260,12 @@ async function disconnectEmail() {
261
260
  @click="revokeSessions(m.userId, m.name || m.email || m.userId)"
262
261
  />
263
262
  </li>
264
- <li v-if="accounts.members.length === 0" class="text-dimmed">
265
- {{ t('layout.accountTeam.members.empty') }}
263
+ <li v-if="accounts.members.length === 0">
264
+ <EmptyState
265
+ compact
266
+ icon="i-lucide-users"
267
+ :title="t('layout.accountTeam.members.empty')"
268
+ />
266
269
  </li>
267
270
  </ul>
268
271
  </section>
@@ -28,7 +28,7 @@ const { t } = useI18n()
28
28
  const accounts = useAccountsStore()
29
29
  const workspace = useWorkspaceStore()
30
30
  const ui = useUiStore()
31
- const toast = useToast()
31
+ const actionToast = useActionToast()
32
32
  const { present } = usePipelineErrorToast()
33
33
  const access = useWorkspaceAccess()
34
34
  const { confirm } = useConfirm()
@@ -191,7 +191,7 @@ async function removeBoard() {
191
191
  busy.value = true
192
192
  try {
193
193
  await workspace.remove(id)
194
- toast.add({ title: t('layout.boardSwitcher.toast.boardDeleted'), icon: 'i-lucide-check' })
194
+ actionToast.success('layout.boardSwitcher.toast.boardDeleted')
195
195
  } catch (e) {
196
196
  present(e, 'layout.boardSwitcher.toast.deleteBoardFailed')
197
197
  } finally {
@@ -12,10 +12,12 @@ const board = useBoardStore()
12
12
  // the sidebar/command entries with zero edits here.
13
13
  const { toolbarItems, invoke: invokeNav } = useNavContributions()
14
14
  const execution = useExecutionStore()
15
+ const views = useWorkViewStore()
16
+ const queue = useWorkspaceQueueStore()
15
17
  const workspace = useWorkspaceStore()
16
18
  const workspaceSettings = useWorkspaceSettingsStore()
17
19
  const services = useServicesStore()
18
- const toast = useToast()
20
+ const actionToast = useActionToast()
19
21
  const { present } = usePipelineErrorToast()
20
22
  const access = useWorkspaceAccess()
21
23
  const { t, n } = useI18n()
@@ -24,11 +26,7 @@ const { fitView, zoomIn, zoomOut, resetZoom } = useBoardFlow()
24
26
  async function mountService(serviceId: string, title: string) {
25
27
  try {
26
28
  await services.mount(serviceId)
27
- toast.add({
28
- title: t('board.toolbar.serviceAdded', { title }),
29
- icon: 'i-lucide-box',
30
- color: 'success',
31
- })
29
+ actionToast.success('board.toolbar.serviceAdded', { params: { title } })
32
30
  } catch (e) {
33
31
  present(e, 'board.toolbar.serviceAddFailed')
34
32
  }
@@ -51,11 +49,7 @@ const mountableItems = computed(() =>
51
49
  async function restoreService(id: string, title: string) {
52
50
  try {
53
51
  await board.restoreService(id)
54
- toast.add({
55
- title: t('board.toast.restored', { name: title }),
56
- icon: 'i-lucide-archive-restore',
57
- color: 'neutral',
58
- })
52
+ actionToast.success('board.toast.restored', { params: { name: title } })
59
53
  } catch (e) {
60
54
  present(e, 'board.toast.restoreFailed')
61
55
  }
@@ -112,19 +106,13 @@ const spendLabel = computed(() => {
112
106
  }
113
107
  return `${fmt(s.costSpent)} / ${fmt(s.costLimit)}`
114
108
  })
115
- const spendColor = computed(() => (spend.value?.exceeded ? 'error' : 'neutral'))
109
+ /** The decisions this counts are in every service, so the queue opens on all of them. */
110
+ function openQueue() {
111
+ queue.clearFilter()
112
+ views.show('queue')
113
+ }
116
114
 
117
- const decisionItems = computed(() =>
118
- execution.openDecisions.map((d) => {
119
- const b = board.getBlock(d.blockId)
120
- return {
121
- label: b?.title ?? t('common.block'),
122
- description: d.decision.question,
123
- icon: 'i-lucide-circle-help',
124
- onSelect: () => ui.openDecision(d.instanceId, d.decision.id),
125
- }
126
- }),
127
- )
115
+ const spendColor = computed(() => (spend.value?.exceeded ? 'error' : 'neutral'))
128
116
  </script>
129
117
 
130
118
  <template>
@@ -199,21 +187,23 @@ const decisionItems = computed(() =>
199
187
 
200
188
  <USeparator orientation="vertical" class="mx-1 h-6" />
201
189
 
202
- <!-- decisions queue -->
203
- <UDropdownMenu v-if="execution.pendingDecisionCount" :items="decisionItems">
204
- <UButton
205
- color="warning"
206
- variant="soft"
207
- size="sm"
208
- icon="i-lucide-circle-help"
209
- data-testid="decision-badge"
190
+ <!-- The decision count. It opens the queue view, which lists every decision with its Answer
191
+ button beside the rest of what needs a person (issue #2258). -->
192
+ <UButton
193
+ v-if="execution.pendingDecisionCount"
194
+ color="warning"
195
+ variant="soft"
196
+ size="sm"
197
+ icon="i-lucide-circle-help"
198
+ :title="t('views.openQueue')"
199
+ data-testid="decision-badge"
200
+ @click="openQueue"
201
+ >
202
+ {{ execution.pendingDecisionCount
203
+ }}<span class="hidden sm:inline"
204
+ >&nbsp;{{ t('board.toolbar.decisionWord', execution.pendingDecisionCount) }}</span
210
205
  >
211
- {{ execution.pendingDecisionCount
212
- }}<span class="hidden sm:inline"
213
- >&nbsp;{{ t('board.toolbar.decisionWord', execution.pendingDecisionCount) }}</span
214
- >
215
- </UButton>
216
- </UDropdownMenu>
206
+ </UButton>
217
207
 
218
208
  <!-- how every frame's swimlanes are ordered + grouped (an override; advanced tier) -->
219
209
  <LaneViewControl />
@@ -33,10 +33,15 @@ import DefaultTestEnvBanner from '~/components/layout/DefaultTestEnvBanner.vue'
33
33
 
34
34
  // The live-stream state the connection strip renders. Passed down rather than resolved here:
35
35
  // the page owns the single stream instance, and creating another would open a second socket.
36
+ //
37
+ // `inFlow` is the queue view's placement: the same region, laid out in the page flow above the
38
+ // queue's columns instead of floating over a canvas, and without the toolbar, which is the
39
+ // canvas's own chrome (zoom, fit, lane view).
36
40
  defineProps<{
37
41
  connected: boolean
38
42
  everConnected: boolean
39
43
  connectionFailed: boolean
44
+ inFlow?: boolean
40
45
  }>()
41
46
 
42
47
  const ui = useUiStore()
@@ -44,7 +49,8 @@ const ui = useUiStore()
44
49
 
45
50
  <template>
46
51
  <div
47
- class="pointer-events-none absolute inset-x-0 top-0 z-40 flex flex-col items-center gap-2 px-4 pt-3"
52
+ class="pointer-events-none z-40 flex flex-col items-center gap-2 px-4"
53
+ :class="inFlow ? 'relative pt-3' : 'absolute inset-x-0 top-0 pt-3'"
48
54
  >
49
55
  <!-- Compact-viewport nav trigger: the SideBar is an off-canvas drawer below lg, so surface
50
56
  a hamburger to open it. Out of the column's flow (it is a corner control, not a
@@ -62,7 +68,7 @@ const ui = useUiStore()
62
68
 
63
69
  <!-- FIRST, and deliberately so: the toolbar is standing board chrome, and a tour step
64
70
  anchors it. Below the advisories it would move every time one appears or clears. -->
65
- <BoardToolbar />
71
+ <BoardToolbar v-if="!inFlow" />
66
72
 
67
73
  <!-- Then the advisories, most urgent first. Ordering is by what the user loses by not
68
74
  reading it now, not by how loud the card is.
@@ -7,6 +7,7 @@
7
7
  // available actions (connected integrations, etc.) show.
8
8
 
9
9
  import SectionLabel from '~/components/common/SectionLabel.vue'
10
+ import EmptyState from '~/components/common/EmptyState.vue'
10
11
 
11
12
  interface Command {
12
13
  id: string
@@ -273,9 +274,12 @@ function indexOf(cmd: Command) {
273
274
  </div>
274
275
 
275
276
  <div class="max-h-80 overflow-y-auto p-1.5">
276
- <p v-if="filtered.length === 0" class="px-3 py-6 text-center text-sm text-dimmed">
277
- {{ t('layout.commandBar.noMatches') }}
278
- </p>
277
+ <EmptyState
278
+ v-if="filtered.length === 0"
279
+ compact
280
+ icon="i-lucide-search-x"
281
+ :title="t('layout.commandBar.noMatches')"
282
+ />
279
283
 
280
284
  <div v-for="group in groups" :key="group.name" class="mb-1">
281
285
  <SectionLabel as="p" class="px-2 pb-1 pt-2">
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onBeforeUnmount, ref, watch } from 'vue'
3
+ import Spinner from '~/components/common/Spinner.vue'
3
4
 
4
5
  // A slim top strip shown when the real-time WebSocket isn't delivering events, so a
5
6
  // silently-frozen board (events stop arriving, nothing updates) is no longer indistinguishable
@@ -70,7 +71,7 @@ onBeforeUnmount(clearTimer)
70
71
  aria-live="polite"
71
72
  data-testid="stream-reconnecting"
72
73
  >
73
- <UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin" />
74
+ <Spinner class="h-3.5 w-3.5" />
74
75
  <span>{{ t('app.reconnecting') }}</span>
75
76
  </div>
76
77
  <div