@cat-factory/app 0.308.0 → 0.310.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 (265) hide show
  1. package/README.md +32 -2
  2. package/app/assets/css/main.css +22 -0
  3. package/app/assets/css/prose.css +4 -4
  4. package/app/assets/css/type.css +24 -0
  5. package/app/components/assistant/AssistantModal.vue +8 -6
  6. package/app/components/auth/BackendMisconfiguredScreen.vue +4 -3
  7. package/app/components/auth/LoginScreen.vue +40 -26
  8. package/app/components/auth/ResetPasswordScreen.vue +9 -4
  9. package/app/components/auth/UserMenu.vue +5 -4
  10. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +48 -37
  11. package/app/components/binaryOutput/BinaryOutputReport.vue +10 -14
  12. package/app/components/binaryOutput/StoredAssetView.vue +13 -11
  13. package/app/components/board/AddTaskModal.vue +30 -27
  14. package/app/components/board/AgentFailureCard.vue +21 -18
  15. package/app/components/board/AgentFailureHistory.vue +18 -9
  16. package/app/components/board/CreateInitiativeModal.vue +7 -6
  17. package/app/components/board/FailureDetail.vue +22 -12
  18. package/app/components/board/FailureHistoryList.vue +5 -5
  19. package/app/components/board/RecurringPipelineModal.vue +12 -11
  20. package/app/components/board/ReviewFrictionDialog.vue +8 -6
  21. package/app/components/board/StepExecutionHistory.vue +8 -8
  22. package/app/components/board/nodes/AgentChip.vue +1 -1
  23. package/app/components/board/nodes/BlockNode.vue +25 -24
  24. package/app/components/board/nodes/DecisionBadge.vue +4 -3
  25. package/app/components/board/nodes/EpicNode.vue +4 -4
  26. package/app/components/board/nodes/FrameSwimlanes.vue +8 -10
  27. package/app/components/board/nodes/InitiativeCard.vue +4 -4
  28. package/app/components/board/nodes/LaneGroup.vue +3 -5
  29. package/app/components/board/nodes/TaskCard.vue +20 -15
  30. package/app/components/board/nodes/TaskLane.vue +3 -3
  31. package/app/components/board/nodes/TaskPipelineMini.vue +21 -18
  32. package/app/components/bootstrap/AdoptionReviewModal.vue +36 -26
  33. package/app/components/bootstrap/BootstrapModal.vue +14 -13
  34. package/app/components/bootstrap/BootstrapRunSteps.vue +1 -1
  35. package/app/components/brainstorm/BrainstormWindow.vue +13 -10
  36. package/app/components/bugFishing/BugFishingWindow.vue +81 -58
  37. package/app/components/clarity/ClarityReviewWindow.vue +13 -10
  38. package/app/components/common/AsyncViewError.vue +1 -1
  39. package/app/components/common/ClarificationItem.vue +6 -6
  40. package/app/components/common/CopyButton.vue +12 -11
  41. package/app/components/common/DescriptorFields.vue +1 -1
  42. package/app/components/common/EmptyState.vue +1 -1
  43. package/app/components/common/IconButton.vue +34 -9
  44. package/app/components/common/InterviewGateNotice.vue +4 -4
  45. package/app/components/common/MarkdownProse.vue +8 -2
  46. package/app/components/common/SectionLabel.vue +38 -0
  47. package/app/components/consensus/ConsensusSessionWindow.vue +10 -9
  48. package/app/components/context/ContextAttachmentFields.vue +32 -20
  49. package/app/components/docs/DocInterviewWindow.vue +10 -9
  50. package/app/components/documents/ContextDocumentPicker.vue +22 -21
  51. package/app/components/documents/DocumentImportModal.vue +7 -5
  52. package/app/components/documents/DocumentOriginLink.vue +12 -9
  53. package/app/components/documents/DocumentSourceConnectModal.vue +2 -2
  54. package/app/components/documents/DocumentSyncState.vue +4 -4
  55. package/app/components/documents/DocumentTemplatesModal.vue +5 -4
  56. package/app/components/documents/RepoContextDocPicker.vue +7 -9
  57. package/app/components/documents/StartFromDesignModal.vue +4 -4
  58. package/app/components/documents/TaskContextDocs.vue +1 -1
  59. package/app/components/environments/EnvSetupStepper.vue +3 -3
  60. package/app/components/environments/EnvironmentStatusPanel.vue +15 -13
  61. package/app/components/environments/steps/EnvPreflightStep.vue +8 -8
  62. package/app/components/environments/steps/EnvReviewStep.vue +23 -22
  63. package/app/components/environments/steps/EnvSaveStep.vue +5 -5
  64. package/app/components/focus/BlockFocusView.vue +11 -10
  65. package/app/components/followUp/FollowUpWindow.vue +18 -15
  66. package/app/components/forkDecision/ForkDecisionWindow.vue +85 -97
  67. package/app/components/foundational/FoundationalContractSummary.vue +2 -2
  68. package/app/components/foundational/FoundationalServiceCatalogList.vue +6 -5
  69. package/app/components/foundational/FoundationalServiceRegistry.vue +10 -9
  70. package/app/components/foundational/FoundationalServiceSources.vue +7 -6
  71. package/app/components/foundational/FoundationalSuppressions.vue +2 -2
  72. package/app/components/fragments/FragmentLibraryManager.vue +8 -7
  73. package/app/components/fragments/FragmentSelector.vue +22 -20
  74. package/app/components/gates/GateFailingCheckList.vue +7 -6
  75. package/app/components/gates/GateResultView.vue +55 -49
  76. package/app/components/github/AddServiceFromRepoModal.vue +16 -11
  77. package/app/components/github/BranchProtectionPreflight.vue +5 -5
  78. package/app/components/github/GitHubConnect.vue +5 -3
  79. package/app/components/github/GitHubOnboarding.vue +7 -2
  80. package/app/components/github/GitHubPanel.vue +23 -19
  81. package/app/components/github/RepoTreeBrowser.vue +16 -13
  82. package/app/components/humanTest/HumanTestWindow.vue +29 -24
  83. package/app/components/initiative/InitiativePlanNotice.vue +3 -3
  84. package/app/components/initiative/InitiativePlanReview.vue +46 -38
  85. package/app/components/initiative/InitiativePlanningWindow.vue +5 -5
  86. package/app/components/initiative/InitiativeTrackerWindow.vue +213 -165
  87. package/app/components/judge/JudgeResultView.vue +28 -27
  88. package/app/components/kaizen/KaizenPanel.vue +70 -56
  89. package/app/components/kaizen/KaizenStepStatus.vue +7 -6
  90. package/app/components/layout/AccountAuditLog.vue +1 -1
  91. package/app/components/layout/AccountDeploymentSettings.vue +10 -10
  92. package/app/components/layout/AccountFailureKindRules.vue +13 -13
  93. package/app/components/layout/AccountFoundationalSettings.vue +1 -1
  94. package/app/components/layout/AccountFragmentSettings.vue +1 -1
  95. package/app/components/layout/AccountModelPolicySettings.vue +7 -7
  96. package/app/components/layout/AccountPlatformAlertSettings.vue +8 -8
  97. package/app/components/layout/AccountRiskPolicySettings.vue +2 -2
  98. package/app/components/layout/AccountRunCredentialSettings.vue +3 -3
  99. package/app/components/layout/AccountSkillSettings.vue +1 -1
  100. package/app/components/layout/AccountTeamSettings.vue +13 -9
  101. package/app/components/layout/AiProvidersBanner.vue +1 -1
  102. package/app/components/layout/AppearanceSwitcher.vue +5 -4
  103. package/app/components/layout/BoardSwitcher.vue +13 -10
  104. package/app/components/layout/BoardToolbar.vue +10 -5
  105. package/app/components/layout/CommandBar.vue +8 -5
  106. package/app/components/layout/DefaultTestEnvBanner.vue +6 -5
  107. package/app/components/layout/IntegrationsHub.vue +12 -9
  108. package/app/components/layout/LanguageSwitcher.vue +5 -4
  109. package/app/components/layout/ModelProvidersHub.vue +10 -7
  110. package/app/components/layout/NotificationsInbox.vue +47 -36
  111. package/app/components/layout/PersonalSetupModal.vue +9 -6
  112. package/app/components/layout/RolePrompt.vue +5 -4
  113. package/app/components/layout/SideBar.vue +7 -8
  114. package/app/components/layout/SpendWarningBanner.vue +3 -3
  115. package/app/components/layout/TranslationWarningBanner.vue +5 -4
  116. package/app/components/layout/UiModeSwitcher.vue +15 -12
  117. package/app/components/layout/UiRoleSwitcher.vue +10 -8
  118. package/app/components/layout/WorkspaceMembersSettings.vue +2 -2
  119. package/app/components/media/ArtifactLightbox.vue +74 -44
  120. package/app/components/media/ImageCompare.vue +80 -85
  121. package/app/components/merge/MergeEffortChips.vue +4 -6
  122. package/app/components/notifications/NotificationSettingsPanel.vue +6 -5
  123. package/app/components/observability/OutcomeFilterChips.vue +6 -5
  124. package/app/components/observability/RunFailureSummary.vue +22 -23
  125. package/app/components/observability/StepMetricsBar.vue +9 -8
  126. package/app/components/observability/StepModelActivity.vue +8 -5
  127. package/app/components/observability/ToolCallList.vue +19 -20
  128. package/app/components/outcome/OutcomeSummaryWindow.vue +48 -46
  129. package/app/components/palettes/AgentPalette.vue +21 -15
  130. package/app/components/palettes/AgentTierSelect.vue +1 -1
  131. package/app/components/palettes/PipelinePurposeSelect.vue +1 -1
  132. package/app/components/panels/AgentStepDetail.vue +57 -49
  133. package/app/components/panels/AttemptEntryHeader.vue +2 -2
  134. package/app/components/panels/DecisionModal.vue +1 -1
  135. package/app/components/panels/GenericStructuredResultView.vue +6 -5
  136. package/app/components/panels/InspectorPanel.vue +15 -13
  137. package/app/components/panels/MergerResultView.vue +10 -9
  138. package/app/components/panels/ObservabilityPanel.vue +211 -186
  139. package/app/components/panels/OperatorDashboardPanel.vue +124 -115
  140. package/app/components/panels/ReportsPanel.vue +87 -83
  141. package/app/components/panels/ReportsSpendBreakdown.vue +2 -2
  142. package/app/components/panels/ResultWindowShell.vue +152 -137
  143. package/app/components/panels/RunDetailLoadState.vue +1 -1
  144. package/app/components/panels/StepContainerStatus.vue +17 -16
  145. package/app/components/panels/StepDelegatedStatus.vue +24 -21
  146. package/app/components/panels/StepEffortReport.vue +12 -17
  147. package/app/components/panels/StepFragmentAdherence.vue +9 -10
  148. package/app/components/panels/StepMetadataCard.vue +41 -43
  149. package/app/components/panels/StepReproductionReport.vue +18 -20
  150. package/app/components/panels/StepRestartControl.vue +3 -2
  151. package/app/components/panels/StepRunMeta.vue +27 -26
  152. package/app/components/panels/StepTestReport.vue +16 -15
  153. package/app/components/panels/StepToolServers.vue +7 -8
  154. package/app/components/panels/StepValidationReport.vue +5 -5
  155. package/app/components/panels/inspector/ContainerSummary.vue +4 -4
  156. package/app/components/panels/inspector/DocReferenceRepos.vue +5 -4
  157. package/app/components/panels/inspector/EnvProbeReport.vue +16 -13
  158. package/app/components/panels/inspector/EpicChildren.vue +12 -10
  159. package/app/components/panels/inspector/FrontendBindingsResolved.vue +5 -4
  160. package/app/components/panels/inspector/FrontendConfig.vue +29 -36
  161. package/app/components/panels/inspector/InitiativeInspector.vue +3 -3
  162. package/app/components/panels/inspector/InspectorSection.vue +25 -17
  163. package/app/components/panels/inspector/RecurringScheduleSettings.vue +9 -8
  164. package/app/components/panels/inspector/ServiceConnections.vue +5 -4
  165. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -2
  166. package/app/components/panels/inspector/ServiceSelfTests.vue +9 -13
  167. package/app/components/panels/inspector/ServiceTestConfig.vue +46 -49
  168. package/app/components/panels/inspector/ServiceTestSecrets.vue +3 -3
  169. package/app/components/panels/inspector/ServiceTestingContext.vue +2 -2
  170. package/app/components/panels/inspector/ServiceValidationConfig.vue +2 -2
  171. package/app/components/panels/inspector/TaskAgentConfig.vue +2 -2
  172. package/app/components/panels/inspector/TaskAprioriBranches.vue +6 -5
  173. package/app/components/panels/inspector/TaskDependencies.vue +1 -1
  174. package/app/components/panels/inspector/TaskEstimateBadge.vue +3 -7
  175. package/app/components/panels/inspector/TaskExecution.vue +18 -16
  176. package/app/components/panels/inspector/TaskRunSettings.vue +45 -44
  177. package/app/components/panels/inspector/TaskStructure.vue +5 -4
  178. package/app/components/pipeline/AgentPromptEditor.vue +15 -14
  179. package/app/components/pipeline/BinaryOutputStepPicker.vue +42 -52
  180. package/app/components/pipeline/EstimateThresholdFields.vue +14 -11
  181. package/app/components/pipeline/GateConfigFields.vue +19 -23
  182. package/app/components/pipeline/IterationCapPrompt.vue +1 -1
  183. package/app/components/pipeline/PipelineBuilder.vue +113 -99
  184. package/app/components/pipeline/PipelineHealthModal.vue +7 -7
  185. package/app/components/pipeline/PipelinePicker.vue +11 -9
  186. package/app/components/pipeline/PipelinePreview.vue +6 -7
  187. package/app/components/pipeline/PipelineProgress.vue +49 -45
  188. package/app/components/prReview/PrReviewWindow.vue +87 -65
  189. package/app/components/providers/AiPresetMismatchDialog.vue +4 -3
  190. package/app/components/providers/AiProviderOnboardingModal.vue +2 -2
  191. package/app/components/providers/ApiKeysSection.vue +11 -9
  192. package/app/components/providers/PersonalSubscriptionSection.vue +4 -3
  193. package/app/components/providers/VendorCredentialsModal.vue +5 -4
  194. package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -9
  195. package/app/components/ralph/RalphLoopResultView.vue +29 -29
  196. package/app/components/requirements/RequirementsReviewWindow.vue +22 -17
  197. package/app/components/riskPolicy/RiskPolicyPicker.vue +13 -11
  198. package/app/components/riskPolicy/RiskPolicyPreview.vue +16 -15
  199. package/app/components/sandbox/SandboxPanel.vue +93 -73
  200. package/app/components/settings/ApiTokensPanel.vue +1 -1
  201. package/app/components/settings/BudgetSettings.vue +18 -17
  202. package/app/components/settings/CapabilityCredentialsPanel.vue +5 -5
  203. package/app/components/settings/CloudflareHandlerSection.vue +44 -41
  204. package/app/components/settings/ComposeEnvironmentSetupSection.vue +14 -12
  205. package/app/components/settings/ConnectionTestVerdict.vue +1 -1
  206. package/app/components/settings/ConsensusGroupsSection.vue +58 -47
  207. package/app/components/settings/CustomManifestTypeEditor.vue +8 -7
  208. package/app/components/settings/DefaultProvisionTypeSection.vue +5 -5
  209. package/app/components/settings/InfraHandlersConfigurator.vue +26 -24
  210. package/app/components/settings/InfrastructureBackendPicker.vue +18 -18
  211. package/app/components/settings/IssueTrackerPanel.vue +30 -21
  212. package/app/components/settings/KubernetesEngineForm.vue +9 -8
  213. package/app/components/settings/KubernetesEnvironmentForm.vue +4 -3
  214. package/app/components/settings/LocalContainerPoolSettings.vue +2 -2
  215. package/app/components/settings/LocalModelEndpointsPanel.vue +13 -11
  216. package/app/components/settings/MergeClassRulesEditor.vue +6 -5
  217. package/app/components/settings/MergeRolePolicyEditor.vue +13 -12
  218. package/app/components/settings/ModelConfigurationPanel.vue +17 -19
  219. package/app/components/settings/ModelPresetHealthModal.vue +3 -3
  220. package/app/components/settings/ObservabilityConnectionPanel.vue +1 -1
  221. package/app/components/settings/OpenRouterCatalogPanel.vue +10 -8
  222. package/app/components/settings/PackageRegistriesPanel.vue +4 -4
  223. package/app/components/settings/ProviderConnectionTab.vue +5 -4
  224. package/app/components/settings/ProviderManifestEditor.vue +9 -8
  225. package/app/components/settings/ProviderPreferenceEditor.vue +13 -13
  226. package/app/components/settings/RiskPolicyCreateForm.vue +40 -26
  227. package/app/components/settings/RiskPolicyEditorRow.vue +21 -19
  228. package/app/components/settings/RiskPolicyHealthModal.vue +3 -3
  229. package/app/components/settings/RiskPolicyPanel.vue +11 -13
  230. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +15 -12
  231. package/app/components/settings/SharedStacksPanel.vue +5 -5
  232. package/app/components/settings/TaskSourceCard.vue +2 -2
  233. package/app/components/settings/TaskTypeSuppressionsPanel.vue +4 -4
  234. package/app/components/settings/ToolServerChecklist.vue +22 -22
  235. package/app/components/settings/UsageSettings.vue +13 -13
  236. package/app/components/settings/UserSecretsSection.vue +4 -3
  237. package/app/components/settings/WorkspaceMetadataSettings.vue +6 -5
  238. package/app/components/settings/WorkspaceSettingsPanel.vue +72 -58
  239. package/app/components/skills/ReviewSkillQueue.vue +12 -10
  240. package/app/components/skills/SkillLibraryManager.vue +9 -8
  241. package/app/components/slack/SlackPanel.vue +17 -12
  242. package/app/components/spec/ServiceSpecWindow.vue +19 -19
  243. package/app/components/tasks/BugHuntModal.vue +1 -1
  244. package/app/components/tasks/ContextIssuePicker.vue +16 -15
  245. package/app/components/tasks/TaskContextIssues.vue +5 -4
  246. package/app/components/tasks/TaskImportModal.vue +3 -2
  247. package/app/components/tasks/TaskSourceConnectModal.vue +5 -5
  248. package/app/components/testing/TestReportWindow.vue +67 -60
  249. package/app/components/theme/ThemeImportModal.vue +2 -2
  250. package/app/components/tutorial/TutorialNudge.vue +3 -2
  251. package/app/components/tutorial/TutorialOverlay.vue +3 -2
  252. package/app/components/vcs/GitLabConnect.vue +1 -1
  253. package/app/components/visualConfirm/VisualConfirmationWindow.vue +79 -54
  254. package/app/utils/theme/builtins.ts +7 -1
  255. package/i18n/locales/de.json +7 -3
  256. package/i18n/locales/en.json +7 -3
  257. package/i18n/locales/es.json +7 -3
  258. package/i18n/locales/fr.json +7 -3
  259. package/i18n/locales/he.json +7 -3
  260. package/i18n/locales/it.json +7 -3
  261. package/i18n/locales/ja.json +7 -3
  262. package/i18n/locales/pl.json +7 -3
  263. package/i18n/locales/tr.json +7 -3
  264. package/i18n/locales/uk.json +7 -3
  265. package/package.json +1 -1
@@ -9,6 +9,7 @@
9
9
  // as a drag-and-drop / click upload target (emits `uploadReference`).
10
10
  import { computed, nextTick, ref, watch } from 'vue'
11
11
  import type { ArtifactBlobs } from '~/composables/useArtifactBlobs'
12
+ import IconButton from '~/components/common/IconButton.vue'
12
13
 
13
14
  const props = defineProps<{
14
15
  view: string
@@ -88,7 +89,7 @@ const diffCanvas = ref<HTMLCanvasElement | null>(null)
88
89
  // there reads as "no difference", the same zero the difference composite produces, in either colour
89
90
  // mode, so no mode-following token fits. Bound as a const so the guard's `fixed-colour-ok:` marker
90
91
  // stays on the class string; the inline-template form gets reflowed off its own line by oxfmt.
91
- const DIFF_CANVAS_CLASS = 'w-full rounded border border-default bg-black' // fixed-colour-ok: diff canvas backdrop
92
+ const DIFF_CANVAS_CLASS = 'w-full rounded-sm border border-default bg-black' // fixed-colour-ok: diff canvas backdrop
92
93
  const CAP = 2000
93
94
  // Bumped on every renderDiff entry so a render whose async work (image decode) is overtaken
94
95
  // by a newer mode/image change bails out instead of drawing stale pixels onto the canvas.
@@ -135,56 +136,60 @@ async function renderDiff() {
135
136
  watch([mode, actualUrl, refUrl], renderDiff, { immediate: true })
136
137
 
137
138
  // --- reference upload (drag-drop + click) ---
138
- const dragOver = ref(false)
139
- const refInput = ref<HTMLInputElement | null>(null)
140
- // Accept only the formats the file input advertises (PNG/JPEG), so a dropped GIF/SVG/WebP
141
- // can't slip past the picker's `accept` filter.
139
+ // `UFileUpload` owns the picker, the drop zone and the drag state; the click-to-replace
140
+ // affordance calls its `open()`. The type guard stays ours: `accept` filters the PICKER, and a
141
+ // dropped GIF/SVG/WebP would otherwise arrive anyway.
142
142
  const ACCEPTED = /^image\/(png|jpeg)$/
143
- function pickFile(files: FileList | null | undefined) {
144
- const file = files?.[0]
143
+ const pendingRef = ref<File | null>(null)
144
+ watch(pendingRef, (file) => {
145
145
  if (file && ACCEPTED.test(file.type)) emit('uploadReference', file)
146
- }
147
- function onDrop(e: DragEvent) {
148
- dragOver.value = false
149
- pickFile(e.dataTransfer?.files)
150
- }
151
- function onRefInput(e: Event) {
152
- pickFile((e.target as HTMLInputElement).files)
153
- if (refInput.value) refInput.value.value = ''
154
- }
146
+ // Cleared either way, so a fresh pick is a fresh file. `UFileUpload` carries `reset` for the
147
+ // other half of this: clearing the model alone leaves the native input holding the file it
148
+ // already has, and the browser fires no change event when the same one is chosen twice.
149
+ if (file) pendingRef.value = null
150
+ })
155
151
  </script>
156
152
 
157
153
  <template>
158
154
  <div class="rounded-lg border border-default bg-default/60 p-3">
159
155
  <div class="mb-2 flex items-center justify-between gap-2">
160
- <h3 class="min-w-0 truncate text-[12px] font-semibold text-default">{{ view }}</h3>
156
+ <h3 class="min-w-0 truncate text-xs font-semibold text-default">{{ view }}</h3>
161
157
  <!-- Mode switch -->
162
158
  <div
163
159
  v-if="MODES.length > 1"
164
160
  class="flex items-center gap-0.5 rounded-md border border-default bg-app-950/60 p-0.5"
165
161
  >
166
- <button
162
+ <IconButton
167
163
  v-for="m in MODES"
168
164
  :key="m.id"
169
- class="rounded px-1.5 py-1 text-muted hover:text-default"
170
- :class="mode === m.id ? 'bg-elevated text-app-100' : ''"
171
- :title="m.label"
165
+ color="neutral"
166
+ variant="ghost"
167
+ :icon="m.icon"
168
+ :label="m.label"
169
+ :aria-pressed="mode === m.id"
170
+ :ui="{
171
+ base: [
172
+ 'rounded-sm px-1.5 py-1 text-muted hover:bg-transparent hover:text-default',
173
+ mode === m.id ? 'bg-elevated text-app-100' : '',
174
+ ].join(' '),
175
+ leadingIcon: 'h-3.5 w-3.5',
176
+ }"
172
177
  @click="mode = m.id"
173
- >
174
- <UIcon :name="m.icon" class="h-3.5 w-3.5" />
175
- </button>
178
+ />
176
179
  </div>
177
180
  </div>
178
181
 
179
182
  <!-- SIDE BY SIDE -->
180
183
  <div v-if="mode === 'side-by-side'" class="grid grid-cols-2 gap-3">
181
184
  <figure class="space-y-1">
182
- <figcaption class="text-[10px] uppercase tracking-wide text-dimmed">
185
+ <figcaption class="text-3xs uppercase tracking-wide text-dimmed">
183
186
  {{ t('media.compare.actual') }}
184
187
  </figcaption>
185
- <button
188
+ <UButton
186
189
  v-if="actualUrl"
187
- class="block w-full overflow-hidden rounded border border-default hover:border-app-600"
190
+ color="neutral"
191
+ variant="ghost"
192
+ class="block w-full overflow-hidden rounded-sm border border-default p-0 hover:border-app-600 hover:bg-transparent"
188
193
  @click="actualId && emit('expand', actualId)"
189
194
  >
190
195
  <img
@@ -192,10 +197,10 @@ function onRefInput(e: Event) {
192
197
  :alt="t('media.compare.actualAlt', { view })"
193
198
  class="w-full cursor-zoom-in"
194
199
  />
195
- </button>
200
+ </UButton>
196
201
  <div
197
202
  v-else
198
- class="flex h-32 items-center justify-center rounded border border-dashed border-muted text-[11px] text-app-600"
203
+ class="flex h-32 items-center justify-center rounded-sm border border-dashed border-muted text-2xs text-app-600"
199
204
  >
200
205
  {{
201
206
  props.blobs.statusFor(actualId) === 'error'
@@ -208,52 +213,51 @@ function onRefInput(e: Event) {
208
213
  </figure>
209
214
 
210
215
  <figure class="space-y-1">
211
- <figcaption class="text-[10px] uppercase tracking-wide text-dimmed">
216
+ <figcaption class="text-3xs uppercase tracking-wide text-dimmed">
212
217
  {{ t('media.compare.reference') }}
213
218
  <span v-if="referenceOrigin === 'design'" class="text-app-warning-300/80">
214
219
  {{ t('media.compare.fromLinkedDesign') }}
215
220
  </span>
216
221
  </figcaption>
217
- <button
218
- v-if="refUrl"
219
- class="group relative block w-full overflow-hidden rounded border border-default hover:border-app-600"
220
- @click="referenceId && emit('expand', referenceId)"
222
+ <UFileUpload
223
+ v-model="pendingRef"
224
+ reset
225
+ accept="image/png,image/jpeg"
226
+ :disabled="busy"
227
+ :preview="false"
228
+ :label="t('media.compare.dropHint')"
229
+ icon="i-lucide-image-up"
230
+ :ui="{ base: 'h-32 text-2xs' }"
221
231
  >
222
- <img
223
- :src="refUrl"
224
- :alt="t('media.compare.referenceAlt', { view })"
225
- class="w-full cursor-zoom-in"
226
- />
227
- <span
228
- class="absolute bottom-1 end-1 rounded bg-app-950/80 px-1.5 py-0.5 text-[10px] text-toned opacity-0 group-hover:opacity-100"
229
- @click.stop="refInput?.click()"
230
- >
231
- {{ t('media.compare.replace') }}
232
- </span>
233
- </button>
234
- <!-- Drop zone when no reference yet -->
235
- <div
236
- v-else
237
- class="flex h-32 cursor-pointer flex-col items-center justify-center gap-1 rounded border border-dashed text-[11px] transition"
238
- :class="
239
- dragOver
240
- ? 'border-app-warning-500 bg-app-warning-500/5 text-app-warning-300'
241
- : 'border-muted text-app-600 hover:border-app-500 hover:text-muted'
242
- "
243
- @click="refInput?.click()"
244
- @dragover.prevent="dragOver = true"
245
- @dragleave.prevent="dragOver = false"
246
- @drop.prevent="onDrop"
247
- >
248
- <UIcon name="i-lucide-image-up" class="h-5 w-5" />
249
- <span>{{ t('media.compare.dropHint') }}</span>
250
- </div>
232
+ <!-- With a reference already in place the tile IS the surface: it opens the lightbox,
233
+ and the hover affordance opens the picker the same drop zone would. -->
234
+ <template v-if="refUrl" #default="{ open }">
235
+ <UButton
236
+ variant="ghost"
237
+ color="neutral"
238
+ class="group relative block w-full overflow-hidden rounded-sm border border-default p-0 hover:border-app-600 hover:bg-transparent"
239
+ @click="referenceId && emit('expand', referenceId)"
240
+ >
241
+ <img
242
+ :src="refUrl"
243
+ :alt="t('media.compare.referenceAlt', { view })"
244
+ class="w-full cursor-zoom-in"
245
+ />
246
+ <span
247
+ class="absolute bottom-1 end-1 rounded-sm bg-app-950/80 px-1.5 py-0.5 text-3xs text-toned opacity-0 group-hover:opacity-100"
248
+ @click.stop="open()"
249
+ >
250
+ {{ t('media.compare.replace') }}
251
+ </span>
252
+ </UButton>
253
+ </template>
254
+ </UFileUpload>
251
255
  </figure>
252
256
  </div>
253
257
 
254
258
  <!-- OVERLAY (onion-skin) -->
255
259
  <div v-else-if="mode === 'overlay'" class="space-y-2">
256
- <div class="relative w-full overflow-hidden rounded border border-default">
260
+ <div class="relative w-full overflow-hidden rounded-sm border border-default">
257
261
  <img :src="refUrl" :alt="t('media.compare.referenceAlt', { view })" class="w-full" />
258
262
  <!-- object-contain so a differing aspect ratio onion-skins undistorted over the reference. -->
259
263
  <img
@@ -263,14 +267,15 @@ function onRefInput(e: Event) {
263
267
  :style="{ opacity: overlayOpacity / 100 }"
264
268
  />
265
269
  </div>
266
- <div class="flex items-center gap-2 text-[10px] uppercase tracking-wide text-dimmed">
270
+ <div class="flex items-center gap-2 text-2xs uppercase tracking-wide text-muted">
267
271
  <span>{{ t('media.compare.reference') }}</span>
268
- <input
269
- v-model.number="overlayOpacity"
270
- type="range"
271
- min="0"
272
- max="100"
273
- class="flex-1 accent-app-warning-500"
272
+ <USlider
273
+ v-model="overlayOpacity"
274
+ :min="0"
275
+ :max="100"
276
+ color="warning"
277
+ size="xs"
278
+ class="flex-1"
274
279
  />
275
280
  <span>{{ t('media.compare.actual') }}</span>
276
281
  </div>
@@ -280,7 +285,7 @@ function onRefInput(e: Event) {
280
285
  <div
281
286
  v-else-if="mode === 'swipe'"
282
287
  ref="swipeBox"
283
- class="relative w-full cursor-ew-resize select-none overflow-hidden rounded border border-default"
288
+ class="relative w-full cursor-ew-resize select-none overflow-hidden rounded-sm border border-default"
284
289
  @pointerdown="onSwipeDown"
285
290
  @pointermove="moveSwipe"
286
291
  @pointerup="onSwipeUp"
@@ -312,11 +317,11 @@ function onRefInput(e: Event) {
312
317
  </span>
313
318
  </div>
314
319
  <span
315
- class="absolute left-1 top-1 rounded bg-app-950/70 px-1 text-[9px] uppercase text-toned"
320
+ class="absolute left-1 top-1 rounded-sm bg-app-950/70 px-1 text-3xs uppercase text-toned"
316
321
  >{{ t('media.compare.actual') }}</span
317
322
  >
318
323
  <span
319
- class="absolute right-1 top-1 rounded bg-app-950/70 px-1 text-[9px] uppercase text-toned"
324
+ class="absolute right-1 top-1 rounded-sm bg-app-950/70 px-1 text-3xs uppercase text-toned"
320
325
  >{{ t('media.compare.reference') }}</span
321
326
  >
322
327
  </div>
@@ -324,17 +329,7 @@ function onRefInput(e: Event) {
324
329
  <!-- DIFF (canvas) -->
325
330
  <div v-else-if="mode === 'diff'" class="space-y-1">
326
331
  <canvas ref="diffCanvas" :class="DIFF_CANVAS_CLASS" />
327
- <p class="text-[10px] text-dimmed">{{ t('media.compare.diffHint') }}</p>
332
+ <p class="text-3xs text-dimmed">{{ t('media.compare.diffHint') }}</p>
328
333
  </div>
329
-
330
- <!-- Hidden file input shared by replace/drop zone -->
331
- <input
332
- ref="refInput"
333
- type="file"
334
- accept="image/png,image/jpeg"
335
- class="hidden"
336
- :disabled="busy"
337
- @change="onRefInput"
338
- />
339
334
  </div>
340
335
  </template>
@@ -10,6 +10,7 @@
10
10
  import { computed } from 'vue'
11
11
  import { REVIEW_EFFORTS } from '@cat-factory/contracts'
12
12
  import type { ChangeClass, MergeClassRollup, ReviewEffort } from '~/types/merge'
13
+ import SectionLabel from '~/components/common/SectionLabel.vue'
13
14
 
14
15
  const props = defineProps<{
15
16
  /** The currently picked effort, or null for "not chosen". */
@@ -87,12 +88,9 @@ function pick(effort: ReviewEffort) {
87
88
  <template>
88
89
  <div data-testid="merge-effort-chips" class="mt-2">
89
90
  <div class="flex items-center gap-1.5">
90
- <span
91
- class="text-[10px] uppercase tracking-wide text-dimmed"
92
- :title="t('merge.effort.promptHint')"
93
- >
91
+ <SectionLabel as="span" :title="t('merge.effort.promptHint')">
94
92
  {{ t('merge.effort.prompt') }}
95
- </span>
93
+ </SectionLabel>
96
94
  <UBadge
97
95
  v-if="classLabel"
98
96
  data-testid="merge-effort-class"
@@ -119,7 +117,7 @@ function pick(effort: ReviewEffort) {
119
117
  {{ chip.label }}
120
118
  </UButton>
121
119
  </div>
122
- <p v-if="history" data-testid="merge-effort-history" class="mt-1 text-[10px] text-dimmed">
120
+ <p v-if="history" data-testid="merge-effort-history" class="mt-1 text-3xs text-dimmed">
123
121
  {{ history }}
124
122
  </p>
125
123
  </div>
@@ -25,6 +25,7 @@ import type {
25
25
  NotificationType,
26
26
  } from '~/types/notifications'
27
27
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
28
+ import SectionLabel from '~/components/common/SectionLabel.vue'
28
29
 
29
30
  const ui = useUiStore()
30
31
  const notifications = useNotificationsStore()
@@ -174,11 +175,11 @@ function resetToDefaults() {
174
175
  </div>
175
176
 
176
177
  <template v-else>
177
- <div class="flex items-center gap-3 px-2 text-[10px] uppercase tracking-wide text-dimmed">
178
+ <SectionLabel class="flex items-center gap-3 px-2">
178
179
  <span class="flex-1">{{ t('notificationSettings.column.event') }}</span>
179
180
  <span class="w-16 text-center">{{ t('notificationSettings.column.inApp') }}</span>
180
181
  <span class="w-16 text-center">{{ t('notificationSettings.column.email') }}</span>
181
- </div>
182
+ </SectionLabel>
182
183
 
183
184
  <div class="max-h-[50vh] space-y-1 overflow-y-auto pr-1">
184
185
  <div
@@ -198,13 +199,13 @@ function resetToDefaults() {
198
199
  </div>
199
200
  </div>
200
201
 
201
- <p class="text-[11px] text-dimmed">
202
+ <p class="text-2xs text-dimmed">
202
203
  {{ t('notificationSettings.panel.inAppNote') }}
203
204
  </p>
204
- <p class="text-[11px] text-dimmed">
205
+ <p class="text-2xs text-dimmed">
205
206
  {{ t('notificationSettings.panel.emailNote') }}
206
207
  </p>
207
- <p class="text-[11px] text-dimmed">
208
+ <p class="text-2xs text-dimmed">
208
209
  {{ t('notificationSettings.panel.otherChannelsNote') }}
209
210
  <UButton
210
211
  v-if="slack.available"
@@ -24,18 +24,19 @@ function toneClass(option: (typeof props.options)[number], active: boolean): str
24
24
  </script>
25
25
 
26
26
  <template>
27
- <div class="flex rounded-lg border border-default p-0.5 text-[12px]">
28
- <button
27
+ <div class="flex rounded-lg border border-default p-0.5 text-xs">
28
+ <UButton
29
+ color="neutral"
30
+ variant="ghost"
29
31
  v-for="option in options"
30
32
  :key="option.value"
31
- type="button"
32
- class="rounded-md px-2.5 py-1 transition"
33
+ class="rounded-md px-2.5 py-1 text-xs transition"
33
34
  :class="toneClass(option, option.value === modelValue)"
34
35
  :aria-pressed="option.value === modelValue"
35
36
  @click="emit('update:modelValue', option.value)"
36
37
  >
37
38
  {{ option.label }}
38
39
  <span class="tabular-nums opacity-70">{{ option.count }}</span>
39
- </button>
40
+ </UButton>
40
41
  </div>
41
42
  </template>
@@ -86,15 +86,15 @@ const earlierFailedToolCalls = computed(() =>
86
86
  <div class="flex items-start gap-3">
87
87
  <UIcon name="i-lucide-siren" class="mt-0.5 h-5 w-5 shrink-0 text-app-error-400" />
88
88
  <div class="min-w-0 flex-1">
89
- <h2 class="text-[13px] font-semibold text-app-error-200">
89
+ <h2 class="text-sm font-semibold text-app-error-200">
90
90
  {{ t('observability.failure.title') }}
91
91
  </h2>
92
92
 
93
93
  <!-- The run's own structured record. Absent on a run that is still going, or that
94
94
  failed without one; the evidence below stands on its own either way. -->
95
95
  <template v-if="evidence.failure">
96
- <p class="mt-1 text-[13px] text-default">{{ evidence.failure.message }}</p>
97
- <div class="mt-1 flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-dimmed">
96
+ <p class="mt-1 text-sm text-default">{{ evidence.failure.message }}</p>
97
+ <div class="mt-1 flex flex-wrap gap-x-3 gap-y-0.5 text-2xs text-dimmed">
98
98
  <span>{{ t('observability.failure.kind', { kind: failureKindLabel }) }}</span>
99
99
  <span v-if="evidence.failure.stepIndex != null">
100
100
  {{ t('observability.failure.atStep', { index: evidence.failure.stepIndex + 1 }) }}
@@ -108,10 +108,10 @@ const earlierFailedToolCalls = computed(() =>
108
108
  <FailureDetail
109
109
  :detail="evidence.failure.detail"
110
110
  :message="evidence.failure.message"
111
- summary-class="text-[11px] text-dimmed hover:text-toned"
112
- pre-class="bg-app-950/60 text-[11px] text-muted"
111
+ summary-class="text-2xs text-dimmed hover:text-toned"
112
+ pre-class="bg-app-950/60 text-2xs text-muted"
113
113
  />
114
- <p v-if="evidence.failure.hint" class="mt-1.5 text-[12px] text-toned">
114
+ <p v-if="evidence.failure.hint" class="mt-1.5 text-xs text-toned">
115
115
  {{ evidence.failure.hint }}
116
116
  </p>
117
117
  </template>
@@ -121,9 +121,10 @@ const earlierFailedToolCalls = computed(() =>
121
121
  <!-- The failing calls themselves. -->
122
122
  <div class="mt-3 space-y-2">
123
123
  <!-- Last model call that FAILED outright. -->
124
- <button
124
+ <UButton
125
+ color="neutral"
126
+ variant="ghost"
125
127
  v-if="evidence.lastErroredCall"
126
- type="button"
127
128
  class="flex w-full items-start gap-3 rounded-lg border border-app-error-900/50 bg-app-950/50 px-3 py-2 text-start transition hover:bg-default/70"
128
129
  @click="emit('showCall', evidence.lastErroredCall.id)"
129
130
  >
@@ -133,7 +134,7 @@ const earlierFailedToolCalls = computed(() =>
133
134
  :style="{ color: agentMeta(evidence.lastErroredCall.agentKind).color }"
134
135
  />
135
136
  <div class="min-w-0 flex-1">
136
- <div class="flex flex-wrap items-baseline gap-x-2 text-[12px]">
137
+ <div class="flex flex-wrap items-baseline gap-x-2 text-xs">
137
138
  <span class="font-medium text-default">
138
139
  {{ t('observability.failure.lastErroredCall') }}
139
140
  </span>
@@ -145,13 +146,10 @@ const earlierFailedToolCalls = computed(() =>
145
146
  {{ evidence.lastErroredCall.httpStatus ?? t('observability.call.error') }}
146
147
  </UBadge>
147
148
  </div>
148
- <p
149
- v-if="evidence.lastErroredCall.errorMessage"
150
- class="mt-0.5 text-[12px] text-app-error-300"
151
- >
149
+ <p v-if="evidence.lastErroredCall.errorMessage" class="mt-0.5 text-xs text-app-error-300">
152
150
  {{ evidence.lastErroredCall.errorMessage }}
153
151
  </p>
154
- <p v-if="evidence.erroredCallCount > 1" class="mt-0.5 text-[11px] text-dimmed">
152
+ <p v-if="evidence.erroredCallCount > 1" class="mt-0.5 text-2xs text-dimmed">
155
153
  {{
156
154
  t(
157
155
  'observability.failure.moreErroredCalls',
@@ -162,18 +160,19 @@ const earlierFailedToolCalls = computed(() =>
162
160
  </p>
163
161
  </div>
164
162
  <UIcon name="i-lucide-chevron-right" class="mt-0.5 h-4 w-4 shrink-0 text-app-600" />
165
- </button>
163
+ </UButton>
166
164
 
167
165
  <!-- Last TOOL call that failed: the row no rollup counts. -->
168
- <button
166
+ <UButton
167
+ color="neutral"
168
+ variant="ghost"
169
169
  v-if="evidence.lastFailedToolCall"
170
- type="button"
171
170
  class="flex w-full items-start gap-3 rounded-lg border border-app-error-900/50 bg-app-950/50 px-3 py-2 text-start transition hover:bg-default/70"
172
171
  @click="emit('showFailingTools')"
173
172
  >
174
173
  <UIcon name="i-lucide-wrench" class="mt-0.5 h-4 w-4 shrink-0 text-app-error-400" />
175
174
  <div class="min-w-0 flex-1">
176
- <div class="flex flex-wrap items-baseline gap-x-2 text-[12px]">
175
+ <div class="flex flex-wrap items-baseline gap-x-2 text-xs">
177
176
  <span class="font-medium text-default">
178
177
  <!-- "One of the failing calls" when even the failures were bounded: the row is
179
178
  real either way, but calling it the LAST would be a claim about rows this
@@ -191,16 +190,16 @@ const earlierFailedToolCalls = computed(() =>
191
190
  </div>
192
191
  <pre
193
192
  v-if="failedToolResult"
194
- class="mt-1 max-h-32 overflow-auto whitespace-pre-wrap text-[11px] leading-relaxed text-app-error-300"
193
+ class="mt-1 max-h-32 overflow-auto whitespace-pre-wrap text-2xs leading-relaxed text-app-error-300"
195
194
  >{{ failedToolResult }}</pre>
196
- <p v-else class="mt-0.5 text-[11px] italic text-dimmed">
195
+ <p v-else class="mt-0.5 text-2xs italic text-dimmed">
197
196
  {{
198
197
  evidence.lastFailedToolCall.bodies === 'stored'
199
198
  ? t('observability.failure.toolReturnedNothing')
200
199
  : t('observability.failure.toolBodiesWithheld')
201
200
  }}
202
201
  </p>
203
- <p v-if="earlierFailedToolCalls" class="mt-0.5 text-[11px] text-dimmed">
202
+ <p v-if="earlierFailedToolCalls" class="mt-0.5 text-2xs text-dimmed">
204
203
  {{
205
204
  t(
206
205
  'observability.failure.moreFailedToolCalls',
@@ -211,7 +210,7 @@ const earlierFailedToolCalls = computed(() =>
211
210
  </p>
212
211
  </div>
213
212
  <UIcon name="i-lucide-chevron-right" class="mt-0.5 h-4 w-4 shrink-0 text-app-600" />
214
- </button>
213
+ </UButton>
215
214
 
216
215
  <!-- Nothing failing to point at. Which of the reasons it is decides what an operator should
217
216
  do next, so each gets its own sentence rather than one shared shrug. `emptyReason` is
@@ -220,7 +219,7 @@ const earlierFailedToolCalls = computed(() =>
220
219
  come back from being reported as one that came back clean. -->
221
220
  <div
222
221
  v-else-if="emptyReason"
223
- class="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-dashed px-3 py-2 text-[12px]"
222
+ class="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-dashed px-3 py-2 text-xs"
224
223
  :class="
225
224
  emptyReason === 'sink-unreachable'
226
225
  ? 'border-app-warning-900/60 text-app-warning-300'
@@ -11,6 +11,7 @@ import {
11
11
  totalInputTokens,
12
12
  transportRatio,
13
13
  } from '~/utils/observability'
14
+ import SectionLabel from '~/components/common/SectionLabel.vue'
14
15
 
15
16
  // Compact, at-a-glance LLM rollup for one pipeline step: token usage, an
16
17
  // output-limit headroom bar (how close the step ran to truncation), a
@@ -57,7 +58,7 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
57
58
  <template>
58
59
  <div
59
60
  v-if="m.calls > 0"
60
- class="rounded-lg border border-default bg-default/40 p-2.5 text-[12px]"
61
+ class="rounded-lg border border-default bg-default/40 p-2.5 text-xs"
61
62
  :class="clickable ? 'cursor-pointer transition hover:border-muted hover:bg-default/70' : ''"
62
63
  :role="clickable ? 'button' : undefined"
63
64
  @click="clickable ? $emit('inspect') : undefined"
@@ -88,13 +89,13 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
88
89
  >
89
90
  {{ onSubscription ? `~${cost}` : cost }}
90
91
  </span>
91
- <span
92
+ <SectionLabel
92
93
  v-if="onSubscription"
93
- class="text-[10px] uppercase tracking-wide text-dimmed"
94
+ as="span"
94
95
  :title="t('observability.metricsBar.subscriptionCostHint')"
95
96
  >
96
97
  {{ t('observability.metricsBar.subscription') }}
97
- </span>
98
+ </SectionLabel>
98
99
  </template>
99
100
  <div class="ms-auto flex items-center gap-1">
100
101
  <UBadge v-if="m.errors > 0" color="error" variant="subtle" size="sm">
@@ -112,7 +113,7 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
112
113
  </div>
113
114
 
114
115
  <!-- input breakdown: the headline's three classes, priced an order of magnitude apart -->
115
- <div v-if="hasCache" class="mt-1.5 flex items-center gap-1.5 text-[11px] tabular-nums">
116
+ <div v-if="hasCache" class="mt-1.5 flex items-center gap-1.5 text-2xs tabular-nums">
116
117
  <span class="text-muted" :title="t('observability.metricsBar.freshHint')">
117
118
  {{ t('observability.metricsBar.fresh', { tokens: formatTokens(m.promptTokens) }) }}
118
119
  </span>
@@ -136,7 +137,7 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
136
137
 
137
138
  <!-- output-limit headroom -->
138
139
  <div v-if="headroom !== null" class="mt-2">
139
- <div class="flex items-center justify-between text-[11px]">
140
+ <div class="flex items-center justify-between text-2xs">
140
141
  <span class="text-dimmed">{{ t('observability.metricsBar.outputLimit') }}</span>
141
142
  <span class="tabular-nums" :class="headroomTone">
142
143
  {{ formatTokens(m.peakCompletionTokens) }} /
@@ -156,7 +157,7 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
156
157
  :style="{ width: `${Math.max(2, pct(headroom))}%` }"
157
158
  />
158
159
  </div>
159
- <p v-if="m.truncatedCalls > 0" class="mt-1 text-[11px] text-app-error-400">
160
+ <p v-if="m.truncatedCalls > 0" class="mt-1 text-2xs text-app-error-400">
160
161
  {{
161
162
  t(
162
163
  'observability.metricsBar.truncatedCalls',
@@ -169,7 +170,7 @@ const headroomTone = computed(() => headroomColor(headroom.value, m.value.trunca
169
170
 
170
171
  <!-- transport overhead vs model execution -->
171
172
  <div v-if="transport !== null" class="mt-2">
172
- <div class="flex items-center justify-between text-[11px]">
173
+ <div class="flex items-center justify-between text-2xs">
173
174
  <span class="text-dimmed">{{ t('observability.metricsBar.transportVsExecution') }}</span>
174
175
  <span class="tabular-nums text-muted">
175
176
  {{ formatMs(m.overheadMs) }} / {{ formatMs(m.upstreamMs) }}
@@ -2,6 +2,7 @@
2
2
  import { computed } from 'vue'
3
3
  import type { StepMetrics } from '~/types/execution'
4
4
  import StepMetricsBar from '~/components/observability/StepMetricsBar.vue'
5
+ import SectionLabel from '~/components/common/SectionLabel.vue'
5
6
 
6
7
  // The shared "Model activity" block: the LLM observability rollup (StepMetricsBar) under
7
8
  // a labelled header with a "View all calls →" link into the full per-call panel. Used by
@@ -31,16 +32,18 @@ function openObservability() {
31
32
  <template>
32
33
  <div v-if="instanceId || hasCalls">
33
34
  <div class="mb-1 flex items-center justify-between">
34
- <span class="text-[11px] font-semibold uppercase tracking-wide text-dimmed">
35
+ <SectionLabel as="span">
35
36
  {{ t('observability.modelActivity') }}
36
- </span>
37
- <button
37
+ </SectionLabel>
38
+ <UButton
39
+ color="neutral"
40
+ variant="ghost"
38
41
  v-if="instanceId"
39
- class="text-[11px] text-app-info-400 hover:text-app-info-300"
42
+ class="p-0 text-2xs text-app-info-400 hover:bg-transparent hover:text-app-info-300"
40
43
  @click="openObservability"
41
44
  >
42
45
  {{ t('observability.viewAllCalls') }}
43
- </button>
46
+ </UButton>
44
47
  </div>
45
48
  <StepMetricsBar
46
49
  v-if="hasCalls && metrics"