@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
@@ -20,6 +20,7 @@ import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.v
20
20
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
21
21
  import GitHubDocUrlImport from '~/components/fragments/GitHubDocUrlImport.vue'
22
22
  import IconButton from '~/components/common/IconButton.vue'
23
+ import EmptyState from '~/components/common/EmptyState.vue'
23
24
 
24
25
  const props = withDefaults(
25
26
  defineProps<{
@@ -41,7 +42,7 @@ const library =
41
42
  : useFragmentLibrary(props.kind, props.ownerId)
42
43
  const documents = useDocumentsStore()
43
44
  const github = useGitHubStore()
44
- const toast = useToast()
45
+ const actionToast = useActionToast()
45
46
  const { present } = usePipelineErrorToast()
46
47
  const { t, d } = useI18n()
47
48
  const { confirm } = useConfirm()
@@ -207,7 +208,7 @@ async function saveEdit() {
207
208
  .filter(Boolean),
208
209
  })
209
210
  editDraft.value = null
210
- toast.add({ title: t('fragments.toast.updated'), icon: 'i-lucide-check' })
211
+ actionToast.success('fragments.toast.updated')
211
212
  } catch (e) {
212
213
  present(e, 'fragments.toast.updateFailed')
213
214
  }
@@ -229,7 +230,7 @@ async function createFragment() {
229
230
  .filter(Boolean),
230
231
  })
231
232
  draft.value = { title: '', summary: '', body: '', brief: '', tags: '' }
232
- toast.add({ title: t('fragments.toast.added'), icon: 'i-lucide-check' })
233
+ actionToast.success('fragments.toast.added')
233
234
  } catch (e) {
234
235
  present(e, 'fragments.toast.addFailed')
235
236
  } finally {
@@ -250,7 +251,7 @@ async function removeFragment(id: string) {
250
251
  await withRow(`remove:${id}`, async () => {
251
252
  try {
252
253
  await library.remove(id)
253
- toast.add({ title: t('fragments.toast.removed'), icon: 'i-lucide-trash-2' })
254
+ actionToast.success('fragments.toast.removed')
254
255
  } catch (e) {
255
256
  present(e, 'fragments.toast.removeFailed')
256
257
  }
@@ -415,7 +416,7 @@ async function linkDocumentFragment() {
415
416
  // Free-text field: exactly one ref.
416
417
  await library.createDocumentFragment({ source, ref: docDraft.value.ref.trim(), tags })
417
418
  docDraft.value = { source: '', ref: '', tags: '' }
418
- toast.add({ title: t('fragments.toast.documentLinked'), icon: 'i-lucide-link' })
419
+ actionToast.success('fragments.toast.documentLinked')
419
420
  return
420
421
  }
421
422
 
@@ -440,9 +441,9 @@ async function linkDocumentFragment() {
440
441
  if (linked > 0) {
441
442
  // A vue-i18n plural message (count as the plural choice) so Slavic few/many forms
442
443
  // render correctly, and the count==1 case reads naturally too.
443
- toast.add({
444
- title: t('fragments.toast.documentsLinked', { count: linked }, linked),
445
- icon: 'i-lucide-link',
444
+ actionToast.success('fragments.toast.documentsLinked', {
445
+ params: { count: linked },
446
+ plural: linked,
446
447
  })
447
448
  }
448
449
  if (firstError) {
@@ -462,7 +463,7 @@ async function refreshFragment(id: string) {
462
463
  await withRow(`refresh:${id}`, async () => {
463
464
  try {
464
465
  await library.refreshDocumentFragment(id)
465
- toast.add({ title: t('fragments.toast.refreshed'), icon: 'i-lucide-refresh-cw' })
466
+ actionToast.success('fragments.toast.refreshed')
466
467
  } catch (e) {
467
468
  present(e, 'fragments.toast.refreshFailed')
468
469
  }
@@ -519,7 +520,7 @@ async function linkSource() {
519
520
  // `syncSource` row wrapper): a failure here should surface as a link failure, and
520
521
  // the form-level `linkingSource` spinner already covers the whole operation.
521
522
  await library.syncSource(source.id)
522
- toast.add({ title: t('fragments.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
523
+ actionToast.success('fragments.toast.sourceLinked')
523
524
  } catch (e) {
524
525
  present(e, 'fragments.toast.linkSourceFailed')
525
526
  } finally {
@@ -531,13 +532,8 @@ async function syncSource(id: string) {
531
532
  await withRow(`sync:${id}`, async () => {
532
533
  try {
533
534
  const result = await library.syncSource(id)
534
- toast.add({
535
- title: t('fragments.toast.synced', {
536
- updated: result.upserted,
537
- removed: result.tombstoned,
538
- }),
539
- icon: 'i-lucide-refresh-cw',
540
- color: 'info',
535
+ actionToast.success('fragments.toast.synced', {
536
+ params: { updated: result.upserted, removed: result.tombstoned },
541
537
  })
542
538
  } catch (e) {
543
539
  present(e, 'fragments.toast.syncFailed')
@@ -549,12 +545,9 @@ async function checkSource(id: string) {
549
545
  await withRow(`check:${id}`, async () => {
550
546
  try {
551
547
  const status = await library.checkSource(id)
552
- toast.add({
553
- title: status.changed
554
- ? t('fragments.toast.changesAvailable')
555
- : t('fragments.toast.upToDate'),
556
- icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
557
- })
548
+ actionToast.info(
549
+ status.changed ? 'fragments.toast.changesAvailable' : 'fragments.toast.upToDate',
550
+ )
558
551
  } catch (e) {
559
552
  present(e, 'fragments.toast.checkSourceFailed')
560
553
  }
@@ -575,7 +568,7 @@ async function unlinkSource(id: string) {
575
568
  await withRow(`unlink:${id}`, async () => {
576
569
  try {
577
570
  await library.unlinkSource(id)
578
- toast.add({ title: t('fragments.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
571
+ actionToast.success('fragments.toast.sourceUnlinked')
579
572
  } catch (e) {
580
573
  present(e, 'fragments.toast.unlinkSourceFailed')
581
574
  }
@@ -771,13 +764,16 @@ async function unlinkSource(id: string) {
771
764
  </div>
772
765
  </div>
773
766
  </div>
774
- <p v-if="!library.fragments.length" class="text-sm text-dimmed">
775
- {{
767
+ <EmptyState
768
+ v-if="!library.fragments.length"
769
+ compact
770
+ icon="i-lucide-puzzle"
771
+ :title="
776
772
  isWorkspace
777
773
  ? t('fragments.authored.empty.workspace')
778
774
  : t('fragments.authored.empty.account')
779
- }}
780
- </p>
775
+ "
776
+ />
781
777
 
782
778
  <div class="rounded-md border border-default p-3">
783
779
  <p class="mb-2 text-sm font-medium">{{ t('fragments.authored.addTitle') }}</p>
@@ -887,9 +883,12 @@ async function unlinkSource(id: string) {
887
883
  />
888
884
  </div>
889
885
  </div>
890
- <p v-if="!documentFragments.length" class="text-sm text-dimmed">
891
- {{ t('fragments.documents.empty') }}
892
- </p>
886
+ <EmptyState
887
+ v-if="!documentFragments.length"
888
+ compact
889
+ icon="i-lucide-file-text"
890
+ :title="t('fragments.documents.empty')"
891
+ />
893
892
 
894
893
  <div class="rounded-md border border-default p-3">
895
894
  <p class="mb-2 text-sm font-medium">{{ t('fragments.documents.linkTitle') }}</p>
@@ -1053,9 +1052,12 @@ async function unlinkSource(id: string) {
1053
1052
  />
1054
1053
  </div>
1055
1054
  </div>
1056
- <p v-if="!library.sources.length" class="text-sm text-dimmed">
1057
- {{ t('fragments.sources.empty') }}
1058
- </p>
1055
+ <EmptyState
1056
+ v-if="!library.sources.length"
1057
+ compact
1058
+ icon="i-lucide-git-branch"
1059
+ :title="t('fragments.sources.empty')"
1060
+ />
1059
1061
 
1060
1062
  <div class="rounded-md border border-default p-3">
1061
1063
  <p class="mb-2 text-sm font-medium">{{ t('fragments.sources.linkTitle') }}</p>
@@ -11,6 +11,7 @@
11
11
  import type { PromptFragment } from '~/types/domain'
12
12
  import { buildFragmentCategoryGroups } from '~/utils/fragmentPicker'
13
13
  import SectionLabel from '~/components/common/SectionLabel.vue'
14
+ import EmptyState from '~/components/common/EmptyState.vue'
14
15
 
15
16
  const props = withDefaults(
16
17
  defineProps<{
@@ -123,9 +124,12 @@ function manageAccount() {
123
124
  </UButton>
124
125
  </div>
125
126
  </template>
126
- <p v-else class="px-2 py-3 text-xs text-dimmed">
127
- {{ t('inspector.fragments.pickerEmpty') }}
128
- </p>
127
+ <EmptyState
128
+ v-else
129
+ compact
130
+ icon="i-lucide-puzzle"
131
+ :title="t('inspector.fragments.pickerEmpty')"
132
+ />
129
133
 
130
134
  <div class="mt-1 border-t border-default pt-1">
131
135
  <UButton
@@ -14,6 +14,7 @@ import AttemptEntryHeader from '~/components/panels/AttemptEntryHeader.vue'
14
14
  import GateFailingCheckList from '~/components/gates/GateFailingCheckList.vue'
15
15
  import CopyButton from '~/components/common/CopyButton.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()
@@ -213,16 +214,12 @@ const conflictVerdict = computed(() => {
213
214
  <div class="flex min-h-0 flex-1">
214
215
  <!-- Main: the conclusion -->
215
216
  <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
216
- <div
217
+ <EmptyState
217
218
  v-if="!gate"
218
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
219
- >
220
- <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
221
- <p class="text-sm">{{ t('gates.noActivity') }}</p>
222
- <p class="max-w-sm text-2xs text-dimmed">
223
- {{ t('gates.noActivityHint') }}
224
- </p>
225
- </div>
219
+ :icon="meta.icon"
220
+ :title="t('gates.noActivity')"
221
+ :description="t('gates.noActivityHint')"
222
+ />
226
223
 
227
224
  <template v-else>
228
225
  <!-- Passed -->
@@ -41,7 +41,7 @@ const ui = useUiStore()
41
41
  const github = useGitHubStore()
42
42
  const board = useBoardStore()
43
43
  const services = useServicesStore()
44
- const toast = useToast()
44
+ const actionToast = useActionToast()
45
45
  const { present } = usePipelineErrorToast()
46
46
  const { freeFramePosition, focusFrame } = useFramePlacement()
47
47
 
@@ -349,11 +349,8 @@ async function add() {
349
349
  // Centre the camera on the newly imported service.
350
350
  await focusFrame(block.id)
351
351
  configuredBlockId.value = block.id
352
- toast.add({
353
- title: t('github.addService.toast.addedTitle'),
352
+ actionToast.success('github.addService.toast.addedTitle', {
354
353
  description: t('github.addService.toast.addedDescription', { title: block.title }),
355
- icon: 'i-lucide-check',
356
- color: 'success',
357
354
  })
358
355
  } catch (e) {
359
356
  present(e, 'github.addService.toast.addFailedTitle')
@@ -417,17 +414,16 @@ async function addServices() {
417
414
  const lastBlockId = created.at(-1)?.blockId
418
415
  if (lastBlockId) await focusFrame(lastBlockId)
419
416
  selectedDirectories.value = []
420
- toast.add({
421
- title: t('github.addService.toast.servicesAddedTitle'),
417
+ const added = {
422
418
  description: [
423
419
  t('github.addService.toast.servicesAddedDescription', { count: dirs.length }, dirs.length),
424
420
  frontendNote(designatedDirectory, wiringLanded),
425
421
  ]
426
422
  .filter(Boolean)
427
423
  .join(' '),
428
- icon: wiringLanded ? 'i-lucide-check' : 'i-lucide-triangle-alert',
429
- color: wiringLanded ? 'success' : 'warning',
430
- })
424
+ }
425
+ if (wiringLanded) actionToast.success('github.addService.toast.servicesAddedTitle', added)
426
+ else actionToast.warning('github.addService.toast.servicesAddedTitle', added)
431
427
  } catch (e) {
432
428
  present(e, 'github.addService.toast.addFailedTitle')
433
429
  } finally {
@@ -7,10 +7,11 @@
7
7
  // react to. Shared by the GitHub panel and the bootstrap modal so the connect
8
8
  // flow lives in one place.
9
9
  import SectionLabel from '~/components/common/SectionLabel.vue'
10
+ import EmptyState from '~/components/common/EmptyState.vue'
10
11
 
11
12
  const { t } = useI18n()
12
13
  const github = useGitHubStore()
13
- const toast = useToast()
14
+ const actionToast = useActionToast()
14
15
  const { present } = usePipelineErrorToast()
15
16
 
16
17
  const installing = ref(false)
@@ -48,11 +49,7 @@ async function connect(id: number, onDone?: () => void) {
48
49
  try {
49
50
  await github.connect(id)
50
51
  onDone?.()
51
- toast.add({
52
- title: t('github.connect.toast.connected'),
53
- icon: 'i-lucide-check',
54
- color: 'success',
55
- })
52
+ actionToast.success('github.connect.toast.connected')
56
53
  } catch (e) {
57
54
  present(e, 'github.connect.errors.connect')
58
55
  } finally {
@@ -90,20 +87,16 @@ async function connectManually() {
90
87
  </UButton>
91
88
  </div>
92
89
 
93
- <div
94
- v-if="github.loadingInstallations && !github.installations.length"
95
- class="flex items-center gap-2 py-3 text-sm text-muted"
96
- >
97
- <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
98
- {{ t('github.connect.lookingForInstallations') }}
90
+ <div v-if="github.loadingInstallations && !github.installations.length" class="space-y-2">
91
+ <USkeleton v-for="i in 2" :key="i" class="h-12 w-full" />
99
92
  </div>
100
93
 
101
- <p
94
+ <EmptyState
102
95
  v-else-if="!github.installations.length"
103
- class="rounded-md border border-dashed border-default px-3 py-3 text-sm text-muted"
104
- >
105
- {{ t('github.connect.noInstallations') }}
106
- </p>
96
+ compact
97
+ icon="i-lucide-github"
98
+ :title="t('github.connect.noInstallations')"
99
+ />
107
100
 
108
101
  <div
109
102
  v-for="inst in github.installations"
@@ -12,12 +12,13 @@ import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
12
12
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
13
13
  import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
14
14
  import SectionLabel from '~/components/common/SectionLabel.vue'
15
+ import EmptyState from '~/components/common/EmptyState.vue'
15
16
 
16
17
  const { t } = useI18n()
17
18
  const ui = useUiStore()
18
19
  const access = useWorkspaceAccess()
19
20
  const github = useGitHubStore()
20
- const toast = useToast()
21
+ const actionToast = useActionToast()
21
22
  const { present } = usePipelineErrorToast()
22
23
  const { confirm } = useConfirm()
23
24
 
@@ -80,6 +81,12 @@ const PULL_TERMS = {
80
81
  },
81
82
  } as const satisfies Record<VcsProvider, Record<string, string>>
82
83
  const pullTerms = computed(() => PULL_TERMS[github.provider])
84
+ // Skeleton rows only while there is nothing to show yet. `github.loading` is also set by every
85
+ // refresh (after a resync, on re-open), and blanking lists the reader is looking at to skeletons
86
+ // and back loses their place for no gain.
87
+ const firstLoad = computed(
88
+ () => github.loading && !github.repos.length && !github.pulls.length && !github.issues.length,
89
+ )
83
90
 
84
91
  // What the repo picker can offer, and why a repo might be missing from it, differ by how the
85
92
  // workspace authenticates rather than by provider: an App installation is shared across the
@@ -110,9 +117,8 @@ async function disconnect() {
110
117
  if (!ok) return
111
118
  try {
112
119
  await github.disconnect()
113
- toast.add({
114
- title: t('vcs.panel.toast.disconnected', { provider: VCS_PROVIDER_LABELS[github.provider] }),
115
- icon: 'i-lucide-unplug',
120
+ actionToast.success('vcs.panel.toast.disconnected', {
121
+ params: { provider: VCS_PROVIDER_LABELS[github.provider] },
116
122
  })
117
123
  } catch (e) {
118
124
  present(e, 'github.panel.errors.disconnect')
@@ -122,11 +128,7 @@ async function disconnect() {
122
128
  async function resync(full = false) {
123
129
  try {
124
130
  const { status } = await github.resync({ full })
125
- toast.add({
126
- title: t('github.panel.toast.resync', { status }),
127
- icon: 'i-lucide-refresh-cw',
128
- color: 'info',
129
- })
131
+ actionToast.info('github.panel.toast.resync', { params: { status } })
130
132
  } catch (e) {
131
133
  present(e, 'github.panel.errors.resync')
132
134
  }
@@ -168,11 +170,7 @@ async function saveRepos() {
168
170
  try {
169
171
  await github.setLinkedRepos([...selected.value])
170
172
  managing.value = false
171
- toast.add({
172
- title: t('github.panel.toast.reposUpdated'),
173
- icon: 'i-lucide-check',
174
- color: 'success',
175
- })
173
+ actionToast.success('github.panel.toast.reposUpdated')
176
174
  } catch (e) {
177
175
  present(e, 'github.panel.errors.updateRepos')
178
176
  }
@@ -207,11 +205,7 @@ async function createBranch(repo: GitHubRepo) {
207
205
  try {
208
206
  await github.createBranch(repo.githubId, { name, fromSha })
209
207
  branchForm.value = { name: '', fromSha: '' }
210
- toast.add({
211
- title: t('github.panel.toast.branchCreated', { name }),
212
- icon: 'i-lucide-check',
213
- color: 'success',
214
- })
208
+ actionToast.success('github.panel.toast.branchCreated', { params: { name } })
215
209
  } catch (e) {
216
210
  present(e, 'github.panel.errors.createBranch')
217
211
  } finally {
@@ -259,7 +253,7 @@ async function openPr() {
259
253
  })
260
254
  showPrForm.value = false
261
255
  prForm.value = { repoGithubId: null, title: '', head: '', base: '' }
262
- toast.add({ title: t(pullTerms.value.opened), icon: 'i-lucide-check', color: 'success' })
256
+ actionToast.success(pullTerms.value.opened)
263
257
  } catch (e) {
264
258
  present(e, pullTerms.value.openFailed)
265
259
  } finally {
@@ -283,11 +277,7 @@ async function merge(pr: GitHubPullRequest) {
283
277
  merging.value = pr.number
284
278
  try {
285
279
  await github.mergePullRequest(pr.repoGithubId, pr.number, { method: 'squash' })
286
- toast.add({
287
- title: t(pullTerms.value.mergedToast, { number: pr.number }),
288
- icon: 'i-lucide-git-merge',
289
- color: 'success',
290
- })
280
+ actionToast.success(pullTerms.value.mergedToast, { params: { number: pr.number } })
291
281
  } catch (e) {
292
282
  present(e, 'github.panel.errors.merge')
293
283
  } finally {
@@ -372,9 +362,8 @@ async function merge(pr: GitHubPullRequest) {
372
362
  </UButton>
373
363
  </div>
374
364
 
375
- <div v-if="github.loading" class="flex items-center gap-2 py-6 text-sm text-muted">
376
- <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
377
- {{ t('github.panel.loading') }}
365
+ <div v-if="firstLoad" class="space-y-2">
366
+ <USkeleton v-for="i in 3" :key="i" class="h-12 w-full" />
378
367
  </div>
379
368
 
380
369
  <!-- repositories -->
@@ -405,20 +394,19 @@ async function merge(pr: GitHubPullRequest) {
405
394
  isAppConnection ? t('vcs.panel.manageHintApp') : t('vcs.panel.manageHintToken')
406
395
  }}
407
396
  </p>
408
- <div
409
- v-if="github.loadingAvailable"
410
- class="flex items-center gap-2 py-3 text-sm text-muted"
411
- >
412
- <UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
413
- {{ t('github.panel.loadingRepos') }}
397
+ <div v-if="github.loadingAvailable" class="space-y-1">
398
+ <USkeleton v-for="i in 4" :key="i" class="h-8 w-full" />
414
399
  </div>
415
- <p v-else-if="!github.availableRepos.length" class="py-2 text-sm text-muted">
416
- {{
400
+ <EmptyState
401
+ v-else-if="!github.availableRepos.length"
402
+ compact
403
+ icon="i-lucide-folder-git-2"
404
+ :title="
417
405
  isAppConnection
418
406
  ? t('vcs.panel.noAvailableReposApp')
419
407
  : t('vcs.panel.noAvailableReposToken')
420
- }}
421
- </p>
408
+ "
409
+ />
422
410
  <div v-else class="max-h-64 space-y-1 overflow-y-auto">
423
411
  <UButton
424
412
  color="neutral"
@@ -463,9 +451,12 @@ async function merge(pr: GitHubPullRequest) {
463
451
  </div>
464
452
  </div>
465
453
 
466
- <p v-if="!github.repos.length && !managing" class="py-4 text-sm text-muted">
467
- {{ t('github.panel.noLinkedRepos') }}
468
- </p>
454
+ <EmptyState
455
+ v-if="!github.repos.length && !managing"
456
+ compact
457
+ icon="i-lucide-folder-git-2"
458
+ :title="t('github.panel.noLinkedRepos')"
459
+ />
469
460
 
470
461
  <!-- Whether the host actually protects what an agent run pushes to. Placed above the
471
462
  repo list because it is a posture check across ALL of them, not a per-repo
@@ -634,9 +625,12 @@ async function merge(pr: GitHubPullRequest) {
634
625
  </div>
635
626
  </div>
636
627
 
637
- <p v-if="!github.pulls.length" class="py-4 text-sm text-muted">
638
- {{ t(pullTerms.none) }}
639
- </p>
628
+ <EmptyState
629
+ v-if="!github.pulls.length"
630
+ compact
631
+ icon="i-lucide-git-pull-request"
632
+ :title="t(pullTerms.none)"
633
+ />
640
634
  <div
641
635
  v-for="pr in github.pulls"
642
636
  :key="`${pr.repoGithubId}-${pr.number}`"
@@ -681,9 +675,12 @@ async function merge(pr: GitHubPullRequest) {
681
675
 
682
676
  <!-- issues -->
683
677
  <section v-else class="space-y-2">
684
- <p v-if="!github.issues.length" class="py-4 text-sm text-muted">
685
- {{ t('github.panel.noIssues') }}
686
- </p>
678
+ <EmptyState
679
+ v-if="!github.issues.length"
680
+ compact
681
+ icon="i-lucide-circle-dot"
682
+ :title="t('github.panel.noIssues')"
683
+ />
687
684
  <div
688
685
  v-for="issue in github.issues"
689
686
  :key="`${issue.repoGithubId}-${issue.number}`"
@@ -19,6 +19,7 @@
19
19
  // service-directory field). Nothing in the tree can be the target then, so the pick is the
20
20
  // folder the caller is standing in and the emitted value is that folder plus the new name.
21
21
  import type { RepoTreeEntry } from '~/types/domain'
22
+ import EmptyState from '~/components/common/EmptyState.vue'
22
23
 
23
24
  const props = withDefaults(
24
25
  defineProps<{
@@ -194,12 +195,15 @@ watch(
194
195
 
195
196
  <!-- listing -->
196
197
  <div class="max-h-56 overflow-auto rounded-sm border border-default">
197
- <div v-if="loading" class="p-3 text-sm text-muted">
198
- {{ t('github.repoTree.loading') }}
199
- </div>
200
- <div v-else-if="isEmpty" class="p-3 text-sm text-muted">
201
- {{ mode === 'dir' ? t('github.repoTree.noSubdirectories') : t('github.repoTree.empty') }}
198
+ <div v-if="loading" class="space-y-1 p-2">
199
+ <USkeleton v-for="i in 4" :key="i" class="h-7 w-full" />
202
200
  </div>
201
+ <EmptyState
202
+ v-else-if="isEmpty"
203
+ compact
204
+ icon="i-lucide-folder-open"
205
+ :title="mode === 'dir' ? t('github.repoTree.noSubdirectories') : t('github.repoTree.empty')"
206
+ />
203
207
  <ul v-else class="divide-y divide-default">
204
208
  <li
205
209
  v-for="entry in dirEntries"
@@ -7,6 +7,7 @@ import type { OpenGuidedReviewInput } from '~/types/domain'
7
7
  import GuidedReviewOverview from '~/components/guidedReview/GuidedReviewOverview.vue'
8
8
  import GuidedReviewThread from '~/components/guidedReview/GuidedReviewThread.vue'
9
9
  import GuidedReviewPicker from '~/components/guidedReview/GuidedReviewPicker.vue'
10
+ import Spinner from '~/components/common/Spinner.vue'
10
11
  import {
11
12
  NEW_THREAD_TAB,
12
13
  isStale,
@@ -175,10 +176,16 @@ const title = computed(() =>
175
176
  @open="openFor"
176
177
  />
177
178
 
178
- <p v-else-if="!session" class="flex items-center gap-2 text-sm text-muted">
179
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
180
- {{ t('guidedReview.loading') }}
181
- </p>
179
+ <div v-else-if="!session" class="flex h-full min-h-0 flex-col gap-4 lg:flex-row">
180
+ <div class="space-y-3 lg:w-2/5 lg:pr-2">
181
+ <USkeleton class="h-6 w-2/3" />
182
+ <USkeleton v-for="n in 4" :key="n" class="h-4 w-full" />
183
+ </div>
184
+ <div class="flex flex-1 flex-col gap-3">
185
+ <USkeleton class="h-8 w-1/2" />
186
+ <USkeleton class="h-48 w-full rounded-lg" />
187
+ </div>
188
+ </div>
182
189
 
183
190
  <div v-else class="flex h-full min-h-0 flex-col gap-4 lg:flex-row">
184
191
  <div class="min-h-0 space-y-3 overflow-y-auto lg:w-2/5 lg:pr-2">
@@ -210,11 +217,14 @@ const title = computed(() =>
210
217
  :aria-selected="tab === activeTab"
211
218
  :variant="tab === activeTab ? 'soft' : 'ghost'"
212
219
  :color="tab === activeTab ? 'primary' : 'neutral'"
213
- :icon="threadBusy(tab) ? 'i-lucide-loader-circle' : undefined"
214
- :ui="{ leadingIcon: threadBusy(tab) ? 'animate-spin' : '' }"
215
220
  :data-testid="`guided-review-tab-${tab}`"
216
221
  @click="activeTab = tab"
217
222
  >
223
+ <!-- Not `:loading`: that disables the tab, and a reader switches back to a thread
224
+ precisely while its reply is pending. -->
225
+ <template v-if="threadBusy(tab)" #leading>
226
+ <Spinner class="size-4 shrink-0" />
227
+ </template>
218
228
  <span class="max-w-48 truncate">{{ threadTitle(tab) }}</span>
219
229
  </UButton>
220
230
  <UButton
@@ -3,6 +3,7 @@
3
3
  // chips the window turns into new threads.
4
4
  import type { GuidedReviewSession } from '~/types/domain'
5
5
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
6
+ import Spinner from '~/components/common/Spinner.vue'
6
7
  import GuidedReviewFailure from '~/components/guidedReview/GuidedReviewFailure.vue'
7
8
  import { citationLabel } from '~/components/guidedReview/GuidedReview.logic'
8
9
 
@@ -53,7 +54,7 @@ const SEVERITY_COLOR = { high: 'error', medium: 'warning', low: 'neutral' } as c
53
54
  class="flex items-center gap-2 text-sm text-muted"
54
55
  data-testid="guided-review-overview-working"
55
56
  >
56
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
57
+ <Spinner class="h-4 w-4" />
57
58
  {{ t('guidedReview.overview.working') }}
58
59
  </p>
59
60
 
@@ -2,6 +2,7 @@
2
2
  // Pick a linked repository and one of its pull requests (an open one from the synced list, or any
3
3
  // number typed in) to open a guided review on.
4
4
  import type { OpenGuidedReviewInput } from '~/types/domain'
5
+ import EmptyState from '~/components/common/EmptyState.vue'
5
6
 
6
7
  defineProps<{ opening: boolean; loading: boolean }>()
7
8
  const emit = defineEmits<{ open: [target: OpenGuidedReviewInput] }>()
@@ -34,13 +35,15 @@ function submit(number = prNumber.value): void {
34
35
  <template>
35
36
  <div class="mx-auto max-w-xl space-y-4" data-testid="guided-review-picker">
36
37
  <p class="text-sm text-muted">{{ t('guidedReview.picker.intro') }}</p>
37
- <p v-if="loading" class="flex items-center gap-2 text-sm text-muted">
38
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
39
- {{ t('guidedReview.picker.loading') }}
40
- </p>
41
- <p v-else-if="!github.repos.length" class="text-sm text-toned">
42
- {{ t('guidedReview.picker.noRepos') }}
43
- </p>
38
+ <div v-if="loading" class="space-y-4">
39
+ <USkeleton class="h-8 w-full" />
40
+ <USkeleton class="h-8 w-full" />
41
+ </div>
42
+ <EmptyState
43
+ v-else-if="!github.repos.length"
44
+ icon="i-lucide-folder-git-2"
45
+ :title="t('guidedReview.picker.noRepos')"
46
+ />
44
47
  <template v-else>
45
48
  <UFormField :label="t('guidedReview.picker.repo')">
46
49
  <USelectMenu