@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
@@ -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
@@ -4,6 +4,8 @@
4
4
  import { isPostableDraft } from '@cat-factory/contracts'
5
5
  import type { GuidedReviewCommentDraft, GuidedReviewThreadView } from '~/types/domain'
6
6
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
7
+ import Spinner from '~/components/common/Spinner.vue'
8
+ import EmptyState from '~/components/common/EmptyState.vue'
7
9
  import GuidedReviewFailure from '~/components/guidedReview/GuidedReviewFailure.vue'
8
10
  import GuidedReviewDraftCard from '~/components/guidedReview/GuidedReviewDraftCard.vue'
9
11
  import {
@@ -112,9 +114,12 @@ async function draftComments(): Promise<void> {
112
114
  <template>
113
115
  <div class="flex h-full min-h-0 flex-col gap-3" data-testid="guided-review-thread">
114
116
  <div class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
115
- <p v-if="!messages.length" class="text-sm text-muted">
116
- {{ t('guidedReview.thread.empty') }}
117
- </p>
117
+ <EmptyState
118
+ v-if="!messages.length"
119
+ compact
120
+ icon="i-lucide-message-circle-question"
121
+ :title="t('guidedReview.thread.empty')"
122
+ />
118
123
  <div
119
124
  v-for="m in messages"
120
125
  :key="m.id"
@@ -129,7 +134,7 @@ async function draftComments(): Promise<void> {
129
134
  <p class="whitespace-pre-wrap">{{ m.content }}</p>
130
135
  </template>
131
136
  <p v-else-if="isLive(m)" class="flex items-center gap-2 text-muted">
132
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
137
+ <Spinner class="h-4 w-4" />
133
138
  {{
134
139
  m.depth === 'deep'
135
140
  ? t('guidedReview.thread.investigating')
@@ -16,6 +16,7 @@ type HumanTestRoundOutcome = NonNullable<HumanTestRound['outcome']>
16
16
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
17
17
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
18
18
  import SectionLabel from '~/components/common/SectionLabel.vue'
19
+ import Spinner from '~/components/common/Spinner.vue'
19
20
 
20
21
  const board = useBoardStore()
21
22
  const execution = useExecutionStore()
@@ -253,7 +254,7 @@ const canDestroy = computed(
253
254
  v-if="working"
254
255
  class="flex items-center gap-2 rounded-lg border border-default bg-app-950/40 px-3 py-2 text-xs text-toned"
255
256
  >
256
- <UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-app-warning-300" />
257
+ <Spinner class="h-3.5 w-3.5 text-app-warning-300" />
257
258
  {{ phase ? t(PHASE_LABEL[phase]) : '' }}
258
259
  </p>
259
260
 
@@ -37,6 +37,7 @@ import {
37
37
  } from '~/utils/interviewGate'
38
38
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
39
39
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
40
+ import EmptyState from '~/components/common/EmptyState.vue'
40
41
 
41
42
  const board = useBoardStore()
42
43
  const initiatives = useInitiativesStore()
@@ -212,13 +213,12 @@ async function onDiscard() {
212
213
  <div class="flex min-h-0 flex-1">
213
214
  <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
214
215
  <!-- No entity yet -->
215
- <div
216
+ <EmptyState
216
217
  v-if="!initiative"
217
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
218
- >
219
- <UIcon name="i-lucide-messages-square" class="h-8 w-8 opacity-40" />
220
- <p class="text-sm">{{ t('initiative.planning.empty') }}</p>
221
- </div>
218
+ class="h-full"
219
+ icon="i-lucide-messages-square"
220
+ :title="t('initiative.planning.empty')"
221
+ />
222
222
 
223
223
  <template v-else>
224
224
  <p class="mb-4 text-sm leading-relaxed text-toned">
@@ -31,6 +31,7 @@ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
31
31
  import InitiativePlanReview from '~/components/initiative/InitiativePlanReview.vue'
32
32
  import InitiativePlanNotice from '~/components/initiative/InitiativePlanNotice.vue'
33
33
  import SectionLabel from '~/components/common/SectionLabel.vue'
34
+ import EmptyState from '~/components/common/EmptyState.vue'
34
35
 
35
36
  const board = useBoardStore()
36
37
  const initiatives = useInitiativesStore()
@@ -373,14 +374,12 @@ const { requestClose } = useUnsavedGuard({
373
374
  <!-- No entity yet (module unwired / still creating). Centred in the column when it is the
374
375
  only thing in it; merely inset when the notice above it means `h-full` would overflow
375
376
  the scroller by the notice's own height. -->
376
- <div
377
+ <EmptyState
377
378
  v-if="!initiative"
378
- class="flex flex-col items-center justify-center gap-2 text-center text-muted"
379
379
  :class="planApproval ? 'py-16' : 'h-full'"
380
- >
381
- <UIcon name="i-lucide-milestone" class="h-8 w-8 opacity-40" />
382
- <p class="text-sm">{{ t('initiative.tracker.empty') }}</p>
383
- </div>
380
+ icon="i-lucide-milestone"
381
+ :title="t('initiative.tracker.empty')"
382
+ />
384
383
 
385
384
  <template v-else>
386
385
  <!-- Paused at a phase checkpoint (D2): a completed checkpoint phase is awaiting
@@ -467,12 +466,13 @@ const { requestClose } = useUnsavedGuard({
467
466
  </section>
468
467
 
469
468
  <!-- Awaiting planning -->
470
- <div
469
+ <EmptyState
471
470
  v-if="phases.length === 0"
472
- class="mb-4 rounded-lg border border-dashed border-muted p-4 text-center text-xs text-muted"
473
- >
474
- {{ t('initiative.tracker.noPlan') }}
475
- </div>
471
+ class="mb-4"
472
+ compact
473
+ icon="i-lucide-list-todo"
474
+ :title="t('initiative.tracker.noPlan')"
475
+ />
476
476
 
477
477
  <!-- Phases + items -->
478
478
  <section v-for="phase in phases" :key="phase.id" class="mb-5">
@@ -14,6 +14,7 @@ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
14
14
  import CopyButton from '~/components/common/CopyButton.vue'
15
15
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
16
16
  import SectionLabel from '~/components/common/SectionLabel.vue'
17
+ import EmptyState from '~/components/common/EmptyState.vue'
17
18
 
18
19
  const board = useBoardStore()
19
20
  const execution = useExecutionStore()
@@ -149,13 +150,7 @@ async function act(choice: 'proceed' | 'bounce' | 'stop') {
149
150
  </template>
150
151
 
151
152
  <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
152
- <div
153
- v-if="!judge"
154
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
155
- >
156
- <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
157
- <p class="text-sm">{{ t('judge.empty') }}</p>
158
- </div>
153
+ <EmptyState v-if="!judge" :icon="meta.icon" :title="t('judge.empty')" />
159
154
 
160
155
  <template v-else>
161
156
  <!-- The score against the task's threshold — the whole verdict in one line. -->
@@ -4,6 +4,7 @@ import { onKeyStroke } from '@vueuse/core'
4
4
  import type { TableColumn } from '@nuxt/ui'
5
5
  import type { KaizenGrading } from '~/types/domain'
6
6
  import { agentKindMeta } from '~/utils/catalog'
7
+ import EmptyState from '~/components/common/EmptyState.vue'
7
8
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
8
9
 
9
10
  // The Kaizen screen: a full-panel overlay listing the workspace's grading history and
@@ -123,7 +124,16 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
123
124
  <p class="mb-3 text-2xs text-dimmed">
124
125
  {{ t('kaizen.verifiedCombos.hint') }}
125
126
  </p>
126
- <ul class="space-y-2">
127
+ <div v-if="kaizen.loadingOverview && kaizen.verified.length === 0" class="space-y-2">
128
+ <USkeleton v-for="n in 3" :key="n" class="h-14 w-full rounded-lg" />
129
+ </div>
130
+ <EmptyState
131
+ v-else-if="kaizen.verified.length === 0"
132
+ compact
133
+ icon="i-lucide-badge-check"
134
+ :title="t('kaizen.verifiedCombos.empty')"
135
+ />
136
+ <ul v-else class="space-y-2">
127
137
  <li
128
138
  v-for="c in kaizen.verified"
129
139
  :key="c.comboKey"
@@ -156,9 +166,6 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
156
166
  }}
157
167
  </div>
158
168
  </li>
159
- <li v-if="kaizen.verified.length === 0" class="text-xs text-app-600">
160
- {{ t('kaizen.verifiedCombos.empty') }}
161
- </li>
162
169
  </ul>
163
170
  </section>
164
171
 
@@ -171,6 +178,7 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
171
178
  <UTable
172
179
  :data="kaizen.history"
173
180
  :columns="historyColumns"
181
+ :loading="kaizen.loadingOverview"
174
182
  :ui="{
175
183
  root: 'overflow-hidden rounded-lg border border-default',
176
184
  base: 'text-xs',
@@ -220,8 +228,15 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
220
228
  </span>
221
229
  </div>
222
230
  </template>
231
+ <!-- UTable draws its loading row only through this slot; without it an empty first
232
+ load falls through to #empty and claims there is no history. -->
233
+ <template #loading>
234
+ <div class="space-y-2">
235
+ <USkeleton v-for="n in 3" :key="n" class="h-8 w-full" />
236
+ </div>
237
+ </template>
223
238
  <template #empty>
224
- <span class="text-app-600">{{ t('kaizen.history.empty') }}</span>
239
+ <EmptyState compact icon="i-lucide-history" :title="t('kaizen.history.empty')" />
225
240
  </template>
226
241
  </UTable>
227
242
  </section>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, watch } from 'vue'
3
3
  import SectionLabel from '~/components/common/SectionLabel.vue'
4
+ import Spinner from '~/components/common/Spinner.vue'
4
5
 
5
6
  // Per-step Kaizen grading status, shown inside the run window (NOT on the board). Reads
6
7
  // the grading for this run's step from the kaizen store, lazily loading the run's
@@ -54,7 +55,7 @@ const tone = computed(() => {
54
55
  <span class="text-muted">{{ t('kaizen.status.scheduled') }}</span>
55
56
  </template>
56
57
  <template v-else-if="grading.status === 'running'">
57
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin text-app-hue-teal" />
58
+ <Spinner class="h-3.5 w-3.5 text-app-hue-teal" />
58
59
  <span class="text-app-hue-teal">{{ t('kaizen.status.grading') }}</span>
59
60
  </template>
60
61
  <template v-else-if="grading.status === 'failed'">