@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
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { showOverrideField } from '~/utils/uiMode'
3
3
  import SectionLabel from '~/components/common/SectionLabel.vue'
4
+ import EmptyState from '~/components/common/EmptyState.vue'
4
5
 
5
6
  // The Integrations hub: a single modal that lists the OPTIONAL external systems the WORKSPACE
6
7
  // can enable or link in — the ones that feed a run its context (source control, documents,
@@ -417,9 +418,12 @@ const filteredGroups = computed<IntegrationGroup[]>(() => {
417
418
  class="w-full"
418
419
  />
419
420
 
420
- <p v-if="!filteredGroups.length" class="px-1 py-6 text-center text-sm text-dimmed">
421
- {{ t('layout.integrationsHub.noMatches', { query }) }}
422
- </p>
421
+ <EmptyState
422
+ v-if="!filteredGroups.length"
423
+ compact
424
+ icon="i-lucide-search-x"
425
+ :title="t('layout.integrationsHub.noMatches', { query })"
426
+ />
423
427
 
424
428
  <section v-for="group in filteredGroups" :key="group.title">
425
429
  <SectionLabel as="h3" class="mb-2 px-1">
@@ -13,6 +13,7 @@
13
13
  // "Back to Model providers" control (IntegrationBackTitle). Layout mirrors the Integrations
14
14
  // hub row-for-row so the two read as siblings.
15
15
  import SectionLabel from '~/components/common/SectionLabel.vue'
16
+ import EmptyState from '~/components/common/EmptyState.vue'
16
17
 
17
18
  const { t } = useI18n()
18
19
  const ui = useUiStore()
@@ -197,9 +198,12 @@ const filteredGroups = computed<ProviderGroup[]>(() => {
197
198
  class="w-full"
198
199
  />
199
200
 
200
- <p v-if="!filteredGroups.length" class="px-1 py-6 text-center text-sm text-dimmed">
201
- {{ t('layout.modelProvidersHub.noMatches', { query }) }}
202
- </p>
201
+ <EmptyState
202
+ v-if="!filteredGroups.length"
203
+ compact
204
+ icon="i-lucide-search-x"
205
+ :title="t('layout.modelProvidersHub.noMatches', { query })"
206
+ />
203
207
 
204
208
  <section v-for="group in filteredGroups" :key="group.title">
205
209
  <SectionLabel as="h3" class="mb-2 px-1">
@@ -16,7 +16,7 @@ const ui = useUiStore()
16
16
  const access = useWorkspaceAccess()
17
17
  const execution = useExecutionStore()
18
18
  const trackRecords = useMergeTrackRecordsStore()
19
- const toast = useToast()
19
+ const actionToast = useActionToast()
20
20
  const { present } = usePipelineErrorToast()
21
21
 
22
22
  const busy = ref<string | null>(null)
@@ -197,11 +197,7 @@ async function act(n: Notification) {
197
197
  // Only the merge cards carry a tag; passing `undefined` elsewhere keeps the historical
198
198
  // no-body act, so a non-merge card's action is completely unchanged.
199
199
  await notifications.act(n.id, collectsEffort(n) ? effortFor(n) : undefined)
200
- toast.add({
201
- title: t('layout.notifications.toast.acted'),
202
- color: 'success',
203
- icon: 'i-lucide-check',
204
- })
200
+ actionToast.success('layout.notifications.toast.acted')
205
201
  } catch (e) {
206
202
  present(e, 'layout.notifications.toast.actFailed')
207
203
  } finally {
@@ -213,11 +209,7 @@ async function dismiss(n: Notification) {
213
209
  busy.value = n.id
214
210
  try {
215
211
  await notifications.dismiss(n.id)
216
- toast.add({
217
- title: t('layout.notifications.toast.dismissed'),
218
- color: 'neutral',
219
- icon: 'i-lucide-check',
220
- })
212
+ actionToast.success('layout.notifications.toast.dismissed')
221
213
  } catch (e) {
222
214
  present(e, 'layout.notifications.toast.dismissFailed')
223
215
  } finally {
@@ -21,6 +21,7 @@ import UserMenu from '~/components/auth/UserMenu.vue'
21
21
  import { useViewport } from '~/composables/useViewport'
22
22
  import type { NavContribution } from '~/modular/nav-contributions'
23
23
  import SectionLabel from '~/components/common/SectionLabel.vue'
24
+ import WorkViewSwitcher from '~/components/layout/WorkViewSwitcher.vue'
24
25
 
25
26
  const { t } = useI18n()
26
27
 
@@ -227,6 +228,10 @@ watch(
227
228
  </div>
228
229
 
229
230
  <div class="contents" @click="onNavAction">
231
+ <!-- The two views of the board (issue #2258), first because they decide what the main
232
+ area shows. -->
233
+ <WorkViewSwitcher :collapsed="railed" />
234
+
230
235
  <!-- Command bar launcher (⌘K) — the primary way to create blocks / pipelines
231
236
  and reach every action below. -->
232
237
  <UButton
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import type { WorkView } from '~/utils/workView'
3
+ import SectionLabel from '~/components/common/SectionLabel.vue'
4
+
5
+ /**
6
+ * The sidebar's "Views" section: the queue and the canvas, as two views of one board.
7
+ *
8
+ * A view SWITCHER, not a list of destinations, which is why it is not a `nav` contribution: a
9
+ * contribution opens something, and these change what the main area shows, so they need the
10
+ * selected state a contribution has no shape for. Shown in every tier and every role: which view
11
+ * a person works in is their choice, and neither view is an override of the other.
12
+ */
13
+ defineProps<{ collapsed: boolean }>()
14
+
15
+ const { t } = useI18n()
16
+ const views = useWorkViewStore()
17
+ const queue = useWorkspaceQueueStore()
18
+
19
+ const ITEMS: { view: WorkView; icon: string; labelKey: string }[] = [
20
+ { view: 'queue', icon: 'i-lucide-list-checks', labelKey: 'views.queue' },
21
+ { view: 'board', icon: 'i-lucide-map', labelKey: 'views.board' },
22
+ ]
23
+ </script>
24
+
25
+ <template>
26
+ <section data-testid="work-view-switcher">
27
+ <SectionLabel v-if="!collapsed" as="h2" class="mb-2 px-1">
28
+ {{ t('views.title') }}
29
+ </SectionLabel>
30
+ <div class="space-y-1.5">
31
+ <UButton
32
+ v-for="item in ITEMS"
33
+ :key="item.view"
34
+ :block="!collapsed"
35
+ :square="collapsed"
36
+ size="sm"
37
+ :color="views.view === item.view ? 'primary' : 'neutral'"
38
+ :variant="views.view === item.view ? 'soft' : 'ghost'"
39
+ :icon="item.icon"
40
+ class="w-full"
41
+ :class="collapsed ? 'justify-center' : 'justify-start'"
42
+ :aria-label="collapsed ? t(item.labelKey) : undefined"
43
+ :aria-current="views.view === item.view ? 'page' : undefined"
44
+ :title="collapsed ? t(item.labelKey) : undefined"
45
+ :data-testid="`work-view-${item.view}`"
46
+ @click="views.show(item.view)"
47
+ >
48
+ <template v-if="!collapsed">
49
+ <span class="flex-1 truncate text-start">{{ t(item.labelKey) }}</span>
50
+ <!-- The queue's count: tasks an agent cannot continue until a person acts. -->
51
+ <UBadge
52
+ v-if="item.view === 'queue' && queue.needsYouCount > 0"
53
+ size="sm"
54
+ color="warning"
55
+ variant="subtle"
56
+ data-testid="work-view-queue-count"
57
+ >
58
+ {{ queue.needsYouCount }}
59
+ </UBadge>
60
+ </template>
61
+ </UButton>
62
+ </div>
63
+ </section>
64
+ </template>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted, ref, watch } from 'vue'
3
3
  import type { WorkspaceAccessMode, WorkspaceRole } from '~/types/domain'
4
+ import EmptyState from '~/components/common/EmptyState.vue'
4
5
 
5
6
  // Workspace-membership management (workspace-rbac initiative, slice 9). The tier BELOW
6
7
  // account tenancy: an account admin restricts a board to an explicit member list (with
@@ -16,7 +17,7 @@ const workspace = useWorkspaceStore()
16
17
  const members = useWorkspaceMembersStore()
17
18
  const accounts = useAccountsStore()
18
19
  const auth = useAuthStore()
19
- const toast = useToast()
20
+ const actionToast = useActionToast()
20
21
  const { present } = usePipelineErrorToast()
21
22
  const { t } = useI18n()
22
23
  const { confirmAction, toastDone } = useConfirmAction()
@@ -108,7 +109,7 @@ async function addMember() {
108
109
  await members.add(props.workspaceId, userId, addRole.value)
109
110
  addUserId.value = undefined
110
111
  addRole.value = 'member'
111
- toast.add({ title: t('layout.workspaceMembers.add.added'), icon: 'i-lucide-user-plus' })
112
+ actionToast.success('layout.workspaceMembers.add.added')
112
113
  } catch (e) {
113
114
  present(e, 'layout.workspaceMembers.errors.add')
114
115
  } finally {
@@ -173,7 +174,9 @@ function memberLabel(userId: string, name?: string | null, email?: string | null
173
174
  <h3 class="mb-2 font-semibold text-highlighted">
174
175
  {{ t('layout.workspaceMembers.roster.title') }}
175
176
  </h3>
176
- <p v-if="!rosterReady" class="text-dimmed">{{ t('common.loading') }}</p>
177
+ <div v-if="!rosterReady" class="space-y-1">
178
+ <USkeleton v-for="i in 3" :key="i" class="h-8 w-full" />
179
+ </div>
177
180
  <ul v-else class="space-y-1" data-testid="workspace-members-roster">
178
181
  <li
179
182
  v-for="m in members.members"
@@ -206,8 +209,12 @@ function memberLabel(userId: string, name?: string | null, email?: string | null
206
209
  />
207
210
  </span>
208
211
  </li>
209
- <li v-if="members.members.length === 0" class="text-dimmed">
210
- {{ t('layout.workspaceMembers.roster.empty') }}
212
+ <li v-if="members.members.length === 0">
213
+ <EmptyState
214
+ compact
215
+ icon="i-lucide-users"
216
+ :title="t('layout.workspaceMembers.roster.empty')"
217
+ />
211
218
  </li>
212
219
  </ul>
213
220
  </section>
@@ -10,6 +10,7 @@ import { computed, ref, watch } from 'vue'
10
10
  import { useModalBehavior } from '@modular-vue/core'
11
11
  import type { ArtifactBlobs } from '~/composables/useArtifactBlobs'
12
12
  import IconButton from '~/components/common/IconButton.vue'
13
+ import Spinner from '~/components/common/Spinner.vue'
13
14
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
14
15
 
15
16
  interface LightboxItem {
@@ -273,10 +274,10 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
273
274
  @pointercancel="onPointerUp"
274
275
  />
275
276
  <div v-else class="flex flex-col items-center gap-2 text-dimmed">
276
- <UIcon
277
- :name="state === 'error' ? 'i-lucide-image-off' : 'i-lucide-loader'"
277
+ <Spinner
278
+ :name="state === 'error' ? 'i-lucide-image-off' : undefined"
279
+ :spinning="state !== 'error'"
278
280
  class="h-8 w-8"
279
- :class="state === 'error' ? '' : 'animate-spin'"
280
281
  />
281
282
  <p class="text-xs">
282
283
  {{ state === 'error' ? t('media.lightbox.failed') : t('media.lightbox.loading') }}
@@ -30,7 +30,7 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
30
30
  const ui = useUiStore()
31
31
  const notifications = useNotificationsStore()
32
32
  const slack = useSlackStore()
33
- const toast = useToast()
33
+ const actionToast = useActionToast()
34
34
  const { present } = usePipelineErrorToast()
35
35
  const { t } = useI18n()
36
36
 
@@ -119,11 +119,7 @@ async function save() {
119
119
  busy.value = true
120
120
  try {
121
121
  await notifications.updateSettings(matrix)
122
- toast.add({
123
- title: t('notificationSettings.toast.saved'),
124
- icon: 'i-lucide-check',
125
- color: 'success',
126
- })
122
+ actionToast.success('notificationSettings.toast.saved')
127
123
  } catch (e) {
128
124
  present(e, 'notificationSettings.error.save')
129
125
  } finally {
@@ -167,11 +163,8 @@ function resetToDefaults() {
167
163
  </UButton>
168
164
  </div>
169
165
 
170
- <div
171
- v-else-if="!editable"
172
- class="rounded-lg border border-muted bg-elevated/40 p-3 text-xs text-muted"
173
- >
174
- {{ t('common.loading') }}
166
+ <div v-else-if="!editable" class="space-y-1">
167
+ <USkeleton v-for="n in 4" :key="n" class="h-10 w-full" />
175
168
  </div>
176
169
 
177
170
  <template v-else>
@@ -6,6 +6,7 @@ import { filterToolCallsByOutcome, formatMs } from '~/utils/observability'
6
6
  import { agentKindMeta } from '~/utils/catalog'
7
7
  import OutcomeFilterChips from '~/components/observability/OutcomeFilterChips.vue'
8
8
  import SectionLabel from '~/components/common/SectionLabel.vue'
9
+ import EmptyState from '~/components/common/EmptyState.vue'
9
10
 
10
11
  // The tool-call TRAJECTORY drill-down: what the run's agents DID, oldest first, in the order
11
12
  // they did it. The sibling of the model-call list, and the one that holds the failures no LLM
@@ -153,13 +154,9 @@ function prettyArgs(raw: string): string {
153
154
 
154
155
  <!-- Every state below is gated on having NOTHING to show: rows already in hand outrank a
155
156
  read still in flight behind them, and outrank one that failed. -->
156
- <p
157
- v-if="source.loading && !visible.length"
158
- class="flex items-center justify-center gap-2 py-8 text-center text-sm text-dimmed"
159
- >
160
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
161
- {{ t('observability.toolCalls.loading') }}
162
- </p>
157
+ <div v-if="source.loading && !visible.length" class="space-y-2">
158
+ <USkeleton v-for="n in 3" :key="n" class="h-10 w-full rounded-xl" />
159
+ </div>
163
160
  <div
164
161
  v-else-if="source.error && !visible.length"
165
162
  class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-app-error-900/60 py-6 text-center text-sm text-app-error-400"
@@ -178,20 +175,18 @@ function prettyArgs(raw: string): string {
178
175
  </div>
179
176
  <!-- "The run made no tool calls" is a claim about the RUN, so it comes off the aggregate,
180
177
  never off an empty prefix that may simply not have loaded. -->
181
- <p
178
+ <EmptyState
182
179
  v-else-if="!counts.all"
183
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
184
- >
185
- {{ t('observability.toolCalls.none') }}
186
- </p>
180
+ icon="i-lucide-wrench"
181
+ :title="t('observability.toolCalls.none')"
182
+ />
187
183
  <!-- Narrowed to nothing is a different statement from recorded nothing, and it is the more
188
184
  reassuring of the two: the operator asked for the failures and there are none. -->
189
- <p
185
+ <EmptyState
190
186
  v-else-if="!visible.length"
191
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
192
- >
193
- {{ t('observability.toolCalls.noneMatching') }}
194
- </p>
187
+ icon="i-lucide-list-filter"
188
+ :title="t('observability.toolCalls.noneMatching')"
189
+ />
195
190
 
196
191
  <template v-else>
197
192
  <!-- What this view is bounded by, when it is. A cap nobody can see is a prefix read as a
@@ -31,6 +31,7 @@ import InputGateNotice from '~/components/inputGate/InputGateNotice.vue'
31
31
  import RunDetailLoadState from '~/components/panels/RunDetailLoadState.vue'
32
32
  import SectionLabel from '~/components/common/SectionLabel.vue'
33
33
  import IconButton from '~/components/common/IconButton.vue'
34
+ import EmptyState from '~/components/common/EmptyState.vue'
34
35
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
35
36
 
36
37
  // Detail overlay for a single pipeline step. Opened by clicking an agent in the
@@ -822,12 +823,11 @@ async function copyOutput() {
822
823
  </section>
823
824
  </template>
824
825
 
825
- <p
826
+ <EmptyState
826
827
  v-else
827
- class="rounded-lg border border-dashed border-default py-6 text-center text-sm text-dimmed"
828
- >
829
- {{ t('panels.stepDetail.noProseOutput') }}
830
- </p>
828
+ icon="i-lucide-file-text"
829
+ :title="t('panels.stepDetail.noProseOutput')"
830
+ />
831
831
  </div>
832
832
  </div>
833
833
  </div>
@@ -12,6 +12,7 @@ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
12
12
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
13
13
  import CopyButton from '~/components/common/CopyButton.vue'
14
14
  import SectionLabel from '~/components/common/SectionLabel.vue'
15
+ import EmptyState from '~/components/common/EmptyState.vue'
15
16
 
16
17
  const board = useBoardStore()
17
18
  const execution = useExecutionStore()
@@ -87,16 +88,12 @@ const customJson = computed<string | null>(() => {
87
88
  ><code>{{ customJson }}</code></pre>
88
89
  </template>
89
90
 
90
- <div
91
+ <EmptyState
91
92
  v-else-if="!step?.output"
92
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
93
- >
94
- <UIcon name="i-lucide-braces" class="h-8 w-8 opacity-40" />
95
- <p class="text-sm">{{ t('panels.structuredResult.noResult') }}</p>
96
- <p class="max-w-sm text-2xs text-dimmed">
97
- {{ t('panels.structuredResult.noResultHint') }}
98
- </p>
99
- </div>
93
+ icon="i-lucide-braces"
94
+ :title="t('panels.structuredResult.noResult')"
95
+ :description="t('panels.structuredResult.noResultHint')"
96
+ />
100
97
  </div>
101
98
 
102
99
  <!-- Sidebar: shared run metadata + observability rollup -->
@@ -11,6 +11,7 @@ import BootstrapRunSteps from '~/components/bootstrap/BootstrapRunSteps.vue'
11
11
  import { BLUEPRINT_AGENT_KIND } from '@cat-factory/contracts'
12
12
  import { VCS_PROVIDER_ICONS } from '~/utils/vcs'
13
13
  import SectionLabel from '~/components/common/SectionLabel.vue'
14
+ import Spinner from '~/components/common/Spinner.vue'
14
15
 
15
16
  const board = useBoardStore()
16
17
  const pipelines = usePipelinesStore()
@@ -500,7 +501,7 @@ const showOriginalDescription = ref(false)
500
501
  >
501
502
  <div class="flex items-center justify-between gap-2">
502
503
  <span class="flex items-center gap-1.5 text-xs text-app-warning-300">
503
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
504
+ <Spinner class="h-3.5 w-3.5" />
504
505
  {{ t('panels.inspector.bootstrapping') }}
505
506
  </span>
506
507
  <div class="flex items-center gap-1.5">
@@ -12,6 +12,7 @@ import StepRunMeta from '~/components/panels/StepRunMeta.vue'
12
12
  import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
13
13
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
14
14
  import SectionLabel from '~/components/common/SectionLabel.vue'
15
+ import EmptyState from '~/components/common/EmptyState.vue'
15
16
 
16
17
  const board = useBoardStore()
17
18
  const execution = useExecutionStore()
@@ -275,13 +276,11 @@ const reasonText = computed(() => {
275
276
  :text="step.output"
276
277
  class="text-sm leading-relaxed text-toned"
277
278
  />
278
- <div
279
+ <EmptyState
279
280
  v-else
280
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-muted"
281
- >
282
- <UIcon name="i-lucide-git-pull-request" class="h-8 w-8 opacity-40" />
283
- <p class="text-sm">{{ t('panels.mergerResult.noResult') }}</p>
284
- </div>
281
+ icon="i-lucide-git-pull-request"
282
+ :title="t('panels.mergerResult.noResult')"
283
+ />
285
284
  </div>
286
285
 
287
286
  <!-- Sidebar: shared run metadata. -->
@@ -32,6 +32,7 @@ import RunFailureSummary from '~/components/observability/RunFailureSummary.vue'
32
32
  import ToolCallList from '~/components/observability/ToolCallList.vue'
33
33
  import SectionLabel from '~/components/common/SectionLabel.vue'
34
34
  import IconButton from '~/components/common/IconButton.vue'
35
+ import EmptyState from '~/components/common/EmptyState.vue'
35
36
  import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
36
37
 
37
38
  /** No run selected: the same empty, NOT-truncated trajectory the store answers with. */
@@ -844,13 +845,9 @@ function exportJson() {
844
845
  </section>
845
846
 
846
847
  <!-- states -->
847
- <p
848
- v-if="loading && !calls.length"
849
- class="flex items-center gap-2 py-8 text-center text-sm text-dimmed justify-center"
850
- >
851
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
852
- {{ t('observability.loadingActivity') }}
853
- </p>
848
+ <div v-if="loading && !calls.length" class="space-y-2">
849
+ <USkeleton v-for="n in 3" :key="n" class="h-10 w-full rounded-xl" />
850
+ </div>
854
851
  <div
855
852
  v-else-if="error"
856
853
  class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-app-error-900/60 py-6 text-center text-sm text-app-error-400"
@@ -867,12 +864,11 @@ function exportJson() {
867
864
  {{ t('common.retry') }}
868
865
  </UButton>
869
866
  </div>
870
- <p
867
+ <EmptyState
871
868
  v-else-if="!calls.length"
872
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
873
- >
874
- {{ t('observability.noCalls') }}
875
- </p>
869
+ icon="i-lucide-activity"
870
+ :title="t('observability.noCalls')"
871
+ />
876
872
 
877
873
  <!-- per-call list, narrowable by outcome -->
878
874
  <template v-else>
@@ -886,12 +882,11 @@ function exportJson() {
886
882
  <!-- Narrowed to nothing reads differently from recorded nothing, and on this
887
883
  surface it is the good news: the operator asked for the failures and there
888
884
  are none. -->
889
- <p
885
+ <EmptyState
890
886
  v-if="!visibleCalls.length"
891
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
892
- >
893
- {{ t('observability.noCallsMatching') }}
894
- </p>
887
+ icon="i-lucide-list-filter"
888
+ :title="t('observability.noCallsMatching')"
889
+ />
895
890
 
896
891
  <ul v-else class="space-y-2">
897
892
  <li
@@ -1092,13 +1087,9 @@ function exportJson() {
1092
1087
 
1093
1088
  <!-- Provided context: the complete context each container agent was given. -->
1094
1089
  <div v-else-if="view === 'context'" class="mx-auto max-w-4xl space-y-5">
1095
- <p
1096
- v-if="contextLoading && !contextSnapshots.length"
1097
- class="flex items-center justify-center gap-2 py-8 text-center text-sm text-dimmed"
1098
- >
1099
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
1100
- {{ t('observability.loadingContext') }}
1101
- </p>
1090
+ <div v-if="contextLoading && !contextSnapshots.length" class="space-y-2">
1091
+ <USkeleton v-for="n in 3" :key="n" class="h-10 w-full rounded-xl" />
1092
+ </div>
1102
1093
  <div
1103
1094
  v-else-if="contextError && !contextSnapshots.length"
1104
1095
  class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-app-error-900/60 py-8 text-center text-sm text-app-error-400"
@@ -1115,12 +1106,11 @@ function exportJson() {
1115
1106
  {{ t('common.retry') }}
1116
1107
  </UButton>
1117
1108
  </div>
1118
- <p
1109
+ <EmptyState
1119
1110
  v-else-if="!contextSnapshots.length"
1120
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
1121
- >
1122
- {{ t('observability.noContext') }}
1123
- </p>
1111
+ icon="i-lucide-file-text"
1112
+ :title="t('observability.noContext')"
1113
+ />
1124
1114
 
1125
1115
  <ul v-else class="space-y-2">
1126
1116
  <li
@@ -1251,19 +1241,14 @@ function exportJson() {
1251
1241
  </span>
1252
1242
  </section>
1253
1243
 
1254
- <p
1255
- v-if="searchLoading && !searchQueries.length"
1256
- class="flex items-center justify-center gap-2 py-8 text-center text-sm text-dimmed"
1257
- >
1258
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
1259
- {{ t('observability.loadingSearch') }}
1260
- </p>
1261
- <p
1244
+ <div v-if="searchLoading && !searchQueries.length" class="space-y-2">
1245
+ <USkeleton v-for="n in 3" :key="n" class="h-10 w-full rounded-xl" />
1246
+ </div>
1247
+ <EmptyState
1262
1248
  v-else-if="!searchQueries.length"
1263
- class="rounded-lg border border-dashed border-default py-8 text-center text-sm text-dimmed"
1264
- >
1265
- {{ t('observability.noSearch') }}
1266
- </p>
1249
+ icon="i-lucide-search"
1250
+ :title="t('observability.noSearch')"
1251
+ />
1267
1252
 
1268
1253
  <div v-else>
1269
1254
  <SectionLabel class="mb-2">