@cat-factory/app 0.309.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 (133) hide show
  1. package/README.md +11 -2
  2. package/app/components/assistant/AssistantModal.vue +5 -4
  3. package/app/components/auth/BackendMisconfiguredScreen.vue +4 -3
  4. package/app/components/auth/LoginScreen.vue +40 -26
  5. package/app/components/auth/ResetPasswordScreen.vue +9 -4
  6. package/app/components/auth/UserMenu.vue +4 -3
  7. package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +34 -23
  8. package/app/components/binaryOutput/StoredAssetView.vue +8 -6
  9. package/app/components/board/AddTaskModal.vue +16 -15
  10. package/app/components/board/AgentFailureCard.vue +12 -9
  11. package/app/components/board/AgentFailureHistory.vue +18 -9
  12. package/app/components/board/CreateInitiativeModal.vue +5 -4
  13. package/app/components/board/FailureDetail.vue +22 -12
  14. package/app/components/board/ReviewFrictionDialog.vue +4 -3
  15. package/app/components/board/nodes/BlockNode.vue +17 -15
  16. package/app/components/board/nodes/DecisionBadge.vue +4 -3
  17. package/app/components/board/nodes/FrameSwimlanes.vue +4 -3
  18. package/app/components/board/nodes/TaskCard.vue +12 -7
  19. package/app/components/board/nodes/TaskPipelineMini.vue +9 -7
  20. package/app/components/bootstrap/AdoptionReviewModal.vue +30 -21
  21. package/app/components/bootstrap/BootstrapModal.vue +3 -3
  22. package/app/components/brainstorm/BrainstormWindow.vue +5 -3
  23. package/app/components/bugFishing/BugFishingWindow.vue +49 -27
  24. package/app/components/clarity/ClarityReviewWindow.vue +5 -3
  25. package/app/components/common/CopyButton.vue +12 -11
  26. package/app/components/common/IconButton.vue +34 -9
  27. package/app/components/context/ContextAttachmentFields.vue +23 -12
  28. package/app/components/documents/ContextDocumentPicker.vue +12 -9
  29. package/app/components/documents/DocumentImportModal.vue +4 -3
  30. package/app/components/documents/DocumentOriginLink.vue +12 -9
  31. package/app/components/documents/DocumentSyncState.vue +3 -3
  32. package/app/components/documents/RepoContextDocPicker.vue +4 -3
  33. package/app/components/environments/EnvironmentStatusPanel.vue +4 -3
  34. package/app/components/followUp/FollowUpWindow.vue +9 -6
  35. package/app/components/forkDecision/ForkDecisionWindow.vue +71 -80
  36. package/app/components/foundational/FoundationalServiceCatalogList.vue +3 -2
  37. package/app/components/foundational/FoundationalServiceRegistry.vue +7 -6
  38. package/app/components/foundational/FoundationalServiceSources.vue +7 -6
  39. package/app/components/fragments/FragmentLibraryManager.vue +5 -4
  40. package/app/components/fragments/FragmentSelector.vue +12 -9
  41. package/app/components/gates/GateFailingCheckList.vue +4 -3
  42. package/app/components/gates/GateResultView.vue +17 -12
  43. package/app/components/github/AddServiceFromRepoModal.vue +11 -7
  44. package/app/components/github/GitHubOnboarding.vue +7 -2
  45. package/app/components/github/GitHubPanel.vue +9 -6
  46. package/app/components/github/RepoTreeBrowser.vue +15 -12
  47. package/app/components/humanTest/HumanTestWindow.vue +13 -9
  48. package/app/components/initiative/InitiativePlanReview.vue +28 -18
  49. package/app/components/initiative/InitiativeTrackerWindow.vue +171 -124
  50. package/app/components/judge/JudgeResultView.vue +5 -3
  51. package/app/components/kaizen/KaizenPanel.vue +67 -53
  52. package/app/components/layout/AccountTeamSettings.vue +9 -9
  53. package/app/components/layout/AppearanceSwitcher.vue +4 -3
  54. package/app/components/layout/BoardSwitcher.vue +10 -8
  55. package/app/components/layout/BoardToolbar.vue +5 -4
  56. package/app/components/layout/CommandBar.vue +4 -3
  57. package/app/components/layout/DefaultTestEnvBanner.vue +4 -3
  58. package/app/components/layout/IntegrationsHub.vue +8 -6
  59. package/app/components/layout/LanguageSwitcher.vue +4 -3
  60. package/app/components/layout/ModelProvidersHub.vue +4 -3
  61. package/app/components/layout/NotificationsInbox.vue +40 -30
  62. package/app/components/layout/PersonalSetupModal.vue +4 -3
  63. package/app/components/layout/RolePrompt.vue +4 -3
  64. package/app/components/layout/SideBar.vue +4 -3
  65. package/app/components/layout/TranslationWarningBanner.vue +4 -3
  66. package/app/components/layout/UiModeSwitcher.vue +8 -6
  67. package/app/components/layout/UiRoleSwitcher.vue +8 -6
  68. package/app/components/layout/WorkspaceMembersSettings.vue +2 -2
  69. package/app/components/media/ArtifactLightbox.vue +69 -39
  70. package/app/components/media/ImageCompare.vue +69 -74
  71. package/app/components/observability/OutcomeFilterChips.vue +5 -4
  72. package/app/components/observability/RunFailureSummary.vue +8 -6
  73. package/app/components/observability/StepModelActivity.vue +5 -3
  74. package/app/components/observability/ToolCallList.vue +4 -2
  75. package/app/components/outcome/OutcomeSummaryWindow.vue +5 -4
  76. package/app/components/palettes/AgentPalette.vue +18 -14
  77. package/app/components/panels/AgentStepDetail.vue +30 -19
  78. package/app/components/panels/ObservabilityPanel.vue +149 -114
  79. package/app/components/panels/OperatorDashboardPanel.vue +99 -91
  80. package/app/components/panels/ReportsPanel.vue +64 -61
  81. package/app/components/panels/ResultWindowShell.vue +148 -134
  82. package/app/components/panels/StepContainerStatus.vue +9 -9
  83. package/app/components/panels/StepDelegatedStatus.vue +11 -9
  84. package/app/components/panels/StepRestartControl.vue +3 -2
  85. package/app/components/panels/inspector/EpicChildren.vue +4 -3
  86. package/app/components/panels/inspector/FrontendConfig.vue +3 -2
  87. package/app/components/panels/inspector/InspectorSection.vue +22 -17
  88. package/app/components/panels/inspector/ServiceConnections.vue +3 -2
  89. package/app/components/panels/inspector/ServiceTestConfig.vue +5 -4
  90. package/app/components/panels/inspector/TaskExecution.vue +5 -4
  91. package/app/components/panels/inspector/TaskRunSettings.vue +10 -6
  92. package/app/components/pipeline/EstimateThresholdFields.vue +13 -10
  93. package/app/components/pipeline/GateConfigFields.vue +14 -18
  94. package/app/components/pipeline/PipelineBuilder.vue +90 -77
  95. package/app/components/pipeline/PipelinePicker.vue +8 -6
  96. package/app/components/pipeline/PipelineProgress.vue +26 -21
  97. package/app/components/prReview/PrReviewWindow.vue +46 -25
  98. package/app/components/providers/ApiKeysSection.vue +4 -3
  99. package/app/components/ralph/RalphLoopResultView.vue +4 -3
  100. package/app/components/requirements/RequirementsReviewWindow.vue +10 -6
  101. package/app/components/riskPolicy/RiskPolicyPicker.vue +8 -6
  102. package/app/components/sandbox/SandboxPanel.vue +73 -54
  103. package/app/components/settings/CloudflareHandlerSection.vue +33 -30
  104. package/app/components/settings/ConsensusGroupsSection.vue +40 -30
  105. package/app/components/settings/InfraHandlersConfigurator.vue +16 -13
  106. package/app/components/settings/InfrastructureBackendPicker.vue +17 -17
  107. package/app/components/settings/IssueTrackerPanel.vue +21 -6
  108. package/app/components/settings/LocalModelEndpointsPanel.vue +3 -2
  109. package/app/components/settings/ModelConfigurationPanel.vue +9 -8
  110. package/app/components/settings/OpenRouterCatalogPanel.vue +4 -3
  111. package/app/components/settings/ProviderPreferenceEditor.vue +5 -6
  112. package/app/components/settings/RiskPolicyEditorRow.vue +3 -2
  113. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +10 -8
  114. package/app/components/skills/ReviewSkillQueue.vue +4 -3
  115. package/app/components/skills/SkillLibraryManager.vue +7 -6
  116. package/app/components/slack/SlackPanel.vue +9 -5
  117. package/app/components/spec/ServiceSpecWindow.vue +4 -3
  118. package/app/components/tasks/ContextIssuePicker.vue +12 -9
  119. package/app/components/tasks/TaskContextIssues.vue +4 -3
  120. package/app/components/testing/TestReportWindow.vue +14 -8
  121. package/app/components/theme/ThemeImportModal.vue +2 -2
  122. package/app/components/visualConfirm/VisualConfirmationWindow.vue +62 -35
  123. package/i18n/locales/de.json +7 -3
  124. package/i18n/locales/en.json +7 -3
  125. package/i18n/locales/es.json +7 -3
  126. package/i18n/locales/fr.json +7 -3
  127. package/i18n/locales/he.json +7 -3
  128. package/i18n/locales/it.json +7 -3
  129. package/i18n/locales/ja.json +7 -3
  130. package/i18n/locales/pl.json +7 -3
  131. package/i18n/locales/tr.json +7 -3
  132. package/i18n/locales/uk.json +7 -3
  133. package/package.json +1 -1
@@ -9,6 +9,7 @@ import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
9
9
  import AgentStopButton from '~/components/board/AgentStopButton.vue'
10
10
  import AdoptionReviewModal from '~/components/bootstrap/AdoptionReviewModal.vue'
11
11
  import BootstrapRunSteps from '~/components/bootstrap/BootstrapRunSteps.vue'
12
+ import IconButton from '~/components/common/IconButton.vue'
12
13
  import { useBlockDrag } from '~/composables/useBlockDrag'
13
14
  import { useFrameStacking } from '~/composables/useFrameStacking'
14
15
  import { useViewport } from '~/composables/useViewport'
@@ -440,27 +441,27 @@ const ITEM_ICON: Record<string, string> = {
440
441
  are `board.write`, hidden for a read-only viewer, who keeps the status badge
441
442
  (the one view-only affordance here). -->
442
443
  <template v-if="access.canWriteBoard.value">
443
- <UButton
444
+ <IconButton
444
445
  class="nodrag"
445
446
  data-testid="frame-add-task"
446
447
  :size="isTouch ? 'sm' : 'xs'"
447
448
  variant="ghost"
448
449
  color="neutral"
449
450
  icon="i-lucide-plus"
450
- :title="t('board.frame.addTaskTitle')"
451
+ :label="t('board.frame.addTaskTitle')"
451
452
  @click.stop="addTask"
452
453
  />
453
- <UButton
454
+ <IconButton
454
455
  v-if="tasks.anyOffered"
455
456
  class="nodrag"
456
457
  :size="isTouch ? 'sm' : 'xs'"
457
458
  variant="ghost"
458
459
  color="neutral"
459
460
  icon="i-lucide-ticket"
460
- :title="t('board.frame.createTaskFromIssueTitle')"
461
+ :label="t('board.frame.createTaskFromIssueTitle')"
461
462
  @click.stop="createTaskFromIssue"
462
463
  />
463
- <UButton
464
+ <IconButton
464
465
  v-if="documents.connectedDesignSources.length > 0"
465
466
  class="nodrag"
466
467
  data-testid="frame-start-from-design"
@@ -468,7 +469,7 @@ const ITEM_ICON: Record<string, string> = {
468
469
  variant="ghost"
469
470
  color="neutral"
470
471
  icon="i-lucide-frame"
471
- :title="t('board.frame.startFromDesignTitle')"
472
+ :label="t('board.frame.startFromDesignTitle')"
472
473
  @click.stop="startFromDesign"
473
474
  />
474
475
  <!-- Recurring pipelines + initiatives are ADVANCED-tier authoring: both plan
@@ -479,7 +480,7 @@ const ITEM_ICON: Record<string, string> = {
479
480
  badges its task card and opens its inspector panel, and an initiative is
480
481
  still a block on the board with its own inspector — so what a basic-mode
481
482
  user loses is the ability to author a new one, not sight of one. -->
482
- <UButton
483
+ <IconButton
483
484
  v-if="uiMode.isAdvanced"
484
485
  class="nodrag"
485
486
  data-testid="frame-add-recurring"
@@ -487,10 +488,10 @@ const ITEM_ICON: Record<string, string> = {
487
488
  variant="ghost"
488
489
  color="neutral"
489
490
  icon="i-lucide-repeat"
490
- :title="t('board.frame.addRecurringTitle')"
491
+ :label="t('board.frame.addRecurringTitle')"
491
492
  @click.stop="addRecurring"
492
493
  />
493
- <UButton
494
+ <IconButton
494
495
  v-if="uiMode.isAdvanced"
495
496
  class="nodrag"
496
497
  data-testid="frame-add-initiative"
@@ -498,7 +499,7 @@ const ITEM_ICON: Record<string, string> = {
498
499
  variant="ghost"
499
500
  color="neutral"
500
501
  icon="i-lucide-milestone"
501
- :title="t('board.frame.createInitiativeTitle')"
502
+ :label="t('board.frame.createInitiativeTitle')"
502
503
  @click.stop="createInitiative"
503
504
  />
504
505
  <!-- Hunting a tracker board for a bug worth adopting is TRIAGE, not intake: it
@@ -506,7 +507,7 @@ const ITEM_ICON: Record<string, string> = {
506
507
  engineer/PM judgement call. So it is dropped for a narrowed role, whose three
507
508
  routes in (a new task, a task from a named ticket, a task from a design) all
508
509
  start from work somebody has already decided to do. -->
509
- <UButton
510
+ <IconButton
510
511
  v-if="tasks.anyOffered && uiRole.fullSurface"
511
512
  class="nodrag"
512
513
  data-testid="frame-hunt-bugs"
@@ -514,7 +515,7 @@ const ITEM_ICON: Record<string, string> = {
514
515
  variant="ghost"
515
516
  color="neutral"
516
517
  icon="i-lucide-radar"
517
- :title="t('board.frame.huntBugsTitle')"
518
+ :label="t('board.frame.huntBugsTitle')"
518
519
  @click.stop="huntBugs"
519
520
  />
520
521
  </template>
@@ -559,15 +560,16 @@ const ITEM_ICON: Record<string, string> = {
559
560
 
560
561
  <FrameSwimlanes v-if="hasTasks" :frame-id="block.id" :lane-body-height="laneBodyHeight" />
561
562
 
562
- <button
563
+ <UButton
564
+ color="neutral"
565
+ variant="ghost"
563
566
  v-if="!hasTasks && access.canWriteBoard.value"
564
- type="button"
565
567
  data-testid="frame-add-task-empty"
566
568
  class="absolute inset-4 flex items-center justify-center gap-1 rounded-lg border border-dashed border-muted text-2xs text-dimmed hover:border-app-500 hover:text-toned"
567
569
  @click.stop="addTask"
568
570
  >
569
571
  <UIcon name="i-lucide-plus" class="h-3.5 w-3.5" /> {{ t('board.frame.addFirstTask') }}
570
- </button>
572
+ </UButton>
571
573
  </div>
572
574
  </div>
573
575
 
@@ -16,8 +16,9 @@ const displayLabel = computed(() => props.label ?? t('board.decisionBadge.decisi
16
16
  </script>
17
17
 
18
18
  <template>
19
- <button
20
- type="button"
19
+ <UButton
20
+ color="neutral"
21
+ variant="ghost"
21
22
  class="board-pulse flex items-center gap-1 rounded-full bg-app-warning-500 px-2 py-0.5 text-xs font-semibold text-app-warning-50 dark:text-app-warning-950 shadow-lg transition hover:bg-app-warning-400"
22
23
  @click.stop="$emit('open')"
23
24
  >
@@ -26,5 +27,5 @@ const displayLabel = computed(() => props.label ?? t('board.decisionBadge.decisi
26
27
  <span v-if="count && count > 1" class="rounded-full bg-app-warning-950/30 px-1">
27
28
  {{ count }}
28
29
  </span>
29
- </button>
30
+ </UButton>
30
31
  </template>
@@ -67,8 +67,9 @@ const withheldNote = computed(() => {
67
67
  is the one lane whose emptiness says nothing a reader needs (a new service has merged
68
68
  nothing, which its three live lanes already show). -->
69
69
  <div v-if="doneLane && doneLane.total > 0" class="rounded-lg bg-default/40">
70
- <button
71
- type="button"
70
+ <UButton
71
+ color="neutral"
72
+ variant="ghost"
72
73
  class="nodrag flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left hover:bg-elevated/40"
73
74
  data-testid="done-lane-toggle"
74
75
  :aria-expanded="!laneView.doneLaneCollapsed"
@@ -99,7 +100,7 @@ const withheldNote = computed(() => {
99
100
  <span v-if="withheldNote" class="ms-auto truncate text-3xs text-dimmed">{{
100
101
  withheldNote
101
102
  }}</span>
102
- </button>
103
+ </UButton>
103
104
 
104
105
  <!-- The archive opens as a WIDE GRID: each group takes the strip's full width and wraps its
105
106
  own cards across it, and the groups stack. Wrapping the GROUPS instead read correctly
@@ -5,6 +5,7 @@ import { STATUS_META, MODULE_META, taskTypeMeta } from '~/utils/catalog'
5
5
  import { composeRunOutcome, hasOutcomeToShow } from '~/utils/runOutcome'
6
6
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
7
7
  import TaskPipelineMini from './TaskPipelineMini.vue'
8
+ import IconButton from '~/components/common/IconButton.vue'
8
9
 
9
10
  const props = defineProps<{ taskId: string }>()
10
11
 
@@ -360,15 +361,19 @@ function selectTask() {
360
361
  {{ statusText }}
361
362
  </span>
362
363
  <!-- drag-to-connect handle: drag onto another task to make it depend on this one -->
363
- <button
364
- type="button"
365
- class="nodrag shrink-0 cursor-crosshair touch-none rounded-full p-0.5 text-dimmed hover:bg-elevated hover:text-app-warning-400 pointer-coarse:p-2.5"
366
- :title="t('board.task.dragToConnect')"
364
+ <IconButton
365
+ color="neutral"
366
+ variant="ghost"
367
+ icon="i-lucide-spline"
368
+ class="nodrag shrink-0"
369
+ :label="t('board.task.dragToConnect')"
370
+ :ui="{
371
+ base: 'cursor-crosshair touch-none rounded-full p-0.5 text-dimmed hover:bg-elevated hover:text-app-warning-400 pointer-coarse:p-2.5',
372
+ leadingIcon: 'h-3 w-3 pointer-coarse:h-5 pointer-coarse:w-5',
373
+ }"
367
374
  @pointerdown.stop="startConnect(task.id, $event)"
368
375
  @click.stop
369
- >
370
- <UIcon name="i-lucide-spline" class="h-3 w-3 pointer-coarse:h-5 pointer-coarse:w-5" />
371
- </button>
376
+ />
372
377
  </div>
373
378
 
374
379
  <!-- title gets a full-width row so long titles wrap to two lines rather than
@@ -107,9 +107,10 @@ const ITEM_ICON: Record<string, string> = {
107
107
  {{ t('board.task.buildSteps') }}
108
108
  </SectionLabel>
109
109
  <div v-for="(s, i) in steps" :key="i" class="rounded-sm bg-default/60 px-1.5 py-1">
110
- <button
111
- type="button"
112
- class="flex w-full cursor-pointer items-center gap-1 rounded-sm text-start focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
110
+ <UButton
111
+ color="neutral"
112
+ variant="ghost"
113
+ class="flex w-full cursor-pointer items-center gap-1 rounded-sm p-0 text-start hover:bg-transparent"
113
114
  :title="`${agentKindMeta(s.agentKind).label} — ${agentKindMeta(s.agentKind).description}\n${t('board.task.clickToViewStep')}`"
114
115
  @click.stop="openStep(i)"
115
116
  >
@@ -141,7 +142,7 @@ const ITEM_ICON: Record<string, string> = {
141
142
  :class="stepSpinning(s) ? 'animate-spin' : ''"
142
143
  :style="{ color: stepVisual(s).color }"
143
144
  />
144
- </button>
145
+ </UButton>
145
146
 
146
147
  <!-- PR reviewer: the precise sub-phase (slicing vs reviewing N/M slices), so the board
147
148
  card tells the reviewer's progress apart from a bare subtask count. Self-hides once
@@ -155,20 +156,21 @@ const ITEM_ICON: Record<string, string> = {
155
156
 
156
157
  <!-- pending approval gate: jump straight to the conclusions reader. Suppressed
157
158
  while a reviewer gate is folding/re-reviewing in the background (no human needed). -->
158
- <button
159
+ <UButton
160
+ color="neutral"
161
+ variant="ghost"
159
162
  v-if="
160
163
  s.approval &&
161
164
  s.approval.status === 'pending' &&
162
165
  instance &&
163
166
  !reviews.isBackground(s.agentKind, props.taskId)
164
167
  "
165
- type="button"
166
168
  class="mt-1 flex w-full items-center justify-center gap-1 rounded-sm bg-app-warning-500 px-1.5 py-0.5 text-3xs font-semibold text-app-warning-50 dark:text-app-warning-950 transition hover:bg-app-warning-400"
167
169
  @click.stop="ui.openApprovalDetail(instance.id, s.approval.id)"
168
170
  >
169
171
  <UIcon name="i-lucide-shield-check" class="h-2.5 w-2.5" />
170
172
  {{ t('board.task.reviewAndApprove') }}
171
- </button>
173
+ </UButton>
172
174
 
173
175
  <!-- per-step subtask progress bar -->
174
176
  <div
@@ -225,31 +225,40 @@ watch(open, (isOpen) => {
225
225
  <p v-if="recordsDropped > 0" class="text-app-warning-300/90">
226
226
  {{ t('bootstrap.adoption.survey.truncated', { count: recordsDropped }) }}
227
227
  </p>
228
- <details>
229
- <summary class="cursor-pointer text-dimmed hover:text-toned">
230
- {{ t('bootstrap.adoption.survey.show') }}
231
- </summary>
232
- <ul class="mt-2 space-y-1">
233
- <!-- Keyed by POSITION: the transcript is append-only and rendered in order, and
228
+ <UCollapsible>
229
+ <template #default="{ open }">
230
+ <UButton
231
+ variant="link"
232
+ color="neutral"
233
+ size="xs"
234
+ :icon="open ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
235
+ :label="t('bootstrap.adoption.survey.show')"
236
+ :ui="{ base: 'w-full justify-start gap-1 p-0 text-dimmed hover:text-toned' }"
237
+ />
238
+ </template>
239
+ <template #content>
240
+ <ul class="mt-2 space-y-1">
241
+ <!-- Keyed by POSITION: the transcript is append-only and rendered in order, and
234
242
  the same path legitimately appears twice (a body refused by the seed and then
235
243
  served to the model, a path the model retried). Keying on the path patched
236
244
  those two rows against each other and rendered a note beside the wrong one. -->
237
- <li
238
- v-for="(entry, index) in surveyReads"
239
- :key="index"
240
- class="flex items-baseline gap-2"
241
- >
242
- <span
243
- class="shrink-0 font-mono text-3xs uppercase"
244
- :class="entry.outcome === 'read' ? 'text-dimmed' : 'text-app-warning-400/80'"
245
+ <li
246
+ v-for="(entry, index) in surveyReads"
247
+ :key="index"
248
+ class="flex items-baseline gap-2"
245
249
  >
246
- {{ t(`bootstrap.adoption.survey.outcome.${entry.outcome}`) }}
247
- </span>
248
- <span class="font-mono text-muted">{{ entry.path }}</span>
249
- <span v-if="entry.note" class="text-app-600">{{ entry.note }}</span>
250
- </li>
251
- </ul>
252
- </details>
250
+ <span
251
+ class="shrink-0 font-mono text-3xs uppercase"
252
+ :class="entry.outcome === 'read' ? 'text-dimmed' : 'text-app-warning-400/80'"
253
+ >
254
+ {{ t(`bootstrap.adoption.survey.outcome.${entry.outcome}`) }}
255
+ </span>
256
+ <span class="font-mono text-muted">{{ entry.path }}</span>
257
+ <span v-if="entry.note" class="text-app-600">{{ entry.note }}</span>
258
+ </li>
259
+ </ul>
260
+ </template>
261
+ </UCollapsible>
253
262
  </div>
254
263
 
255
264
  <!-- The reviewer's own instructions, on BOTH paths. With no suggestion to answer this is
@@ -22,6 +22,7 @@ import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
22
22
  import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
23
23
  import { appInstallationManageUrl, newRepoUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
24
24
  import SectionLabel from '~/components/common/SectionLabel.vue'
25
+ import IconButton from '~/components/common/IconButton.vue'
25
26
 
26
27
  const ui = useUiStore()
27
28
  const bootstrap = useBootstrapStore()
@@ -701,13 +702,12 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
701
702
  :placeholder="t('bootstrap.monorepo.directory.placeholder')"
702
703
  class="flex-1"
703
704
  />
704
- <UButton
705
+ <IconButton
705
706
  v-if="monorepoRepoId !== undefined"
706
707
  variant="soft"
707
708
  color="neutral"
708
709
  icon="i-lucide-folder-search"
709
- :title="t('bootstrap.monorepo.directory.browse')"
710
- :aria-label="t('bootstrap.monorepo.directory.browse')"
710
+ :label="t('bootstrap.monorepo.directory.browse')"
711
711
  data-testid="bootstrap-directory-browse"
712
712
  @click="toggleDirectoryBrowse()"
713
713
  />
@@ -465,9 +465,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
465
465
  `width` prop): the window is `full`-width now, and this is continuous prose that
466
466
  would otherwise run to 200-character lines. -->
467
467
  <div v-for="s in outline.sections" :key="s.id" class="mb-2 max-w-3xl">
468
- <button
468
+ <UButton
469
+ color="neutral"
470
+ variant="ghost"
469
471
  v-if="s.title"
470
- class="group flex w-full items-center gap-2 text-start"
472
+ class="group flex w-full items-center gap-2 p-0 text-start hover:bg-transparent"
471
473
  @click="toggle(s.id)"
472
474
  >
473
475
  <UIcon
@@ -480,7 +482,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
480
482
  :class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-xs'"
481
483
  v-html="s.titleHtml"
482
484
  />
483
- </button>
485
+ </UButton>
484
486
  <div
485
487
  v-show="!s.title || !collapsed[s.id]"
486
488
  class="reader-prose mt-1 ps-5.5 text-sm leading-relaxed text-toned"
@@ -267,8 +267,9 @@ const PHASE_ICON: Record<string, string> = {
267
267
  <SectionLabel as="p" class="mb-2 px-1">
268
268
  {{ t('bugFishing.phases.heading') }}
269
269
  </SectionLabel>
270
- <button
271
- type="button"
270
+ <UButton
271
+ color="neutral"
272
+ variant="ghost"
272
273
  class="mb-1 w-full rounded-md px-2 py-1.5 text-left text-xs"
273
274
  :class="
274
275
  selectedPassKey === null
@@ -278,7 +279,7 @@ const PHASE_ICON: Record<string, string> = {
278
279
  @click="selectedPassKey = null"
279
280
  >
280
281
  {{ t('bugFishing.phases.all', { count: findings.length }) }}
281
- </button>
282
+ </UButton>
282
283
  <!-- Grouped by TERRITORY on a partitioned codebase. A codebase small enough to fish
283
284
  whole has one group with no label, which renders as the flat angle rail. -->
284
285
  <div v-for="group in territoryGroups" :key="group.id ?? 'whole'" class="mb-2">
@@ -291,8 +292,9 @@ const PHASE_ICON: Record<string, string> = {
291
292
  </p>
292
293
  <ul class="space-y-0.5">
293
294
  <li v-for="phase in group.passes" :key="passKey(phase)">
294
- <button
295
- type="button"
295
+ <UButton
296
+ color="neutral"
297
+ variant="ghost"
296
298
  class="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left"
297
299
  :class="
298
300
  selectedPassKey === passKey(phase)
@@ -341,7 +343,7 @@ const PHASE_ICON: Record<string, string> = {
341
343
  </span>
342
344
  </span>
343
345
  </span>
344
- </button>
346
+ </UButton>
345
347
  </li>
346
348
  </ul>
347
349
  </div>
@@ -450,10 +452,7 @@ const PHASE_ICON: Record<string, string> = {
450
452
  <p class="text-2xs text-muted">
451
453
  {{ t('bugFishing.counts', { untriaged: untriagedCount, spawned: spawnedCount }) }}
452
454
  </p>
453
- <label class="flex items-center gap-1.5 text-2xs text-muted">
454
- <input v-model="showTriaged" type="checkbox" class="accent-app-info-500" />
455
- {{ t('bugFishing.showTriaged') }}
456
- </label>
455
+ <UCheckbox v-model="showTriaged" size="xs" :label="t('bugFishing.showTriaged')" />
457
456
  </div>
458
457
 
459
458
  <p v-if="bugFishing.error" class="mb-3 text-xs text-app-error-300">
@@ -520,19 +519,41 @@ const PHASE_ICON: Record<string, string> = {
520
519
  <!-- Evidence is rendered apart from the detail for the reason the contract keeps them
521
520
  apart: a finding that cannot point at the code it describes is speculating, and
522
521
  that should be visible without reading the prose for it. -->
523
- <details v-if="finding.evidence" class="mt-2">
524
- <summary class="cursor-pointer text-2xs text-muted hover:text-default">
525
- {{ t('bugFishing.finding.evidence') }}
526
- </summary>
527
- <MarkdownProse :text="finding.evidence" class="mt-1 max-w-3xl text-xs" />
528
- </details>
529
-
530
- <details v-if="finding.suggestedFix" class="mt-1">
531
- <summary class="cursor-pointer text-2xs text-muted hover:text-default">
532
- {{ t('bugFishing.finding.suggestedFix') }}
533
- </summary>
534
- <MarkdownProse :text="finding.suggestedFix" class="mt-1 max-w-3xl text-xs" />
535
- </details>
522
+ <UCollapsible v-if="finding.evidence" class="mt-2">
523
+ <template #default="{ open }">
524
+ <UButton
525
+ variant="link"
526
+ color="neutral"
527
+ size="xs"
528
+ :icon="open ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
529
+ :label="t('bugFishing.finding.evidence')"
530
+ :ui="{
531
+ base: 'w-full justify-start gap-1 p-0 text-2xs text-muted hover:text-default',
532
+ }"
533
+ />
534
+ </template>
535
+ <template #content>
536
+ <MarkdownProse :text="finding.evidence" class="mt-1 max-w-3xl text-xs" />
537
+ </template>
538
+ </UCollapsible>
539
+
540
+ <UCollapsible v-if="finding.suggestedFix" class="mt-1">
541
+ <template #default="{ open }">
542
+ <UButton
543
+ variant="link"
544
+ color="neutral"
545
+ size="xs"
546
+ :icon="open ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
547
+ :label="t('bugFishing.finding.suggestedFix')"
548
+ :ui="{
549
+ base: 'w-full justify-start gap-1 p-0 text-2xs text-muted hover:text-default',
550
+ }"
551
+ />
552
+ </template>
553
+ <template #content>
554
+ <MarkdownProse :text="finding.suggestedFix" class="mt-1 max-w-3xl text-xs" />
555
+ </template>
556
+ </UCollapsible>
536
557
 
537
558
  <!-- Already marked: say what was created and let the reader follow it. The three
538
559
  spawn states are rendered apart because they are three different facts — a task
@@ -553,13 +574,14 @@ const PHASE_ICON: Record<string, string> = {
553
574
  })
554
575
  }}
555
576
  </span>
556
- <button
557
- type="button"
558
- class="underline hover:text-app-success-200"
577
+ <UButton
578
+ color="neutral"
579
+ variant="link"
580
+ class="p-0 text-2xs underline hover:text-app-success-200"
559
581
  @click="openSpawnedTask(finding.spawn.taskId)"
560
582
  >
561
583
  {{ t('bugFishing.finding.openTask') }}
562
- </button>
584
+ </UButton>
563
585
  </div>
564
586
 
565
587
  <!-- A claim held by a marking still in flight. No task to link yet, and no Fix
@@ -491,9 +491,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
491
491
  `width` prop): the window is `full`-width now, and this is continuous prose that
492
492
  would otherwise run to 200-character lines. -->
493
493
  <div v-for="s in outline.sections" :key="s.id" class="mb-2 max-w-3xl">
494
- <button
494
+ <UButton
495
+ color="neutral"
496
+ variant="ghost"
495
497
  v-if="s.title"
496
- class="group flex w-full items-center gap-2 text-start"
498
+ class="group flex w-full items-center gap-2 p-0 text-start hover:bg-transparent"
497
499
  @click="toggle(s.id)"
498
500
  >
499
501
  <UIcon
@@ -506,7 +508,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
506
508
  :class="s.depth <= 1 ? 'text-base' : s.depth === 2 ? 'text-sm' : 'text-xs'"
507
509
  v-html="s.titleHtml"
508
510
  />
509
- </button>
511
+ </UButton>
510
512
  <div
511
513
  v-show="!s.title || !collapsed[s.id]"
512
514
  class="reader-prose mt-1 ps-5.5 text-sm leading-relaxed text-toned"
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  // Standard icon-only "copy to clipboard" button with confirmation feedback. Routes through
3
3
  // `useCopyToClipboard` so success/failure is always toasted (UX-38), and carries both a
4
- // `title` and an `aria-label` so it's named for pointer tooltips and screen readers alike.
5
- // Used to make error/detail surfaces copyable (UX-39) — the first thing a user does with a
4
+ // tooltip and an `aria-label` so it's named for pointer users and screen readers alike.
5
+ // Used to make error/detail surfaces copyable (UX-39): the first thing a user does with a
6
6
  // stack trace or failure summary is copy it.
7
7
  const props = defineProps<{
8
8
  /** The text to place on the clipboard. */
@@ -18,13 +18,14 @@ const label = computed(() => props.label ?? t('common.copy'))
18
18
  </script>
19
19
 
20
20
  <template>
21
- <UButton
22
- icon="i-lucide-copy"
23
- color="neutral"
24
- variant="ghost"
25
- :size="size ?? 'xs'"
26
- :title="label"
27
- :aria-label="label"
28
- @click.stop="copy(text)"
29
- />
21
+ <UTooltip :text="label">
22
+ <UButton
23
+ icon="i-lucide-copy"
24
+ color="neutral"
25
+ variant="ghost"
26
+ :size="size ?? 'xs'"
27
+ :aria-label="label"
28
+ @click.stop="copy(text)"
29
+ />
30
+ </UTooltip>
30
31
  </template>
@@ -1,19 +1,44 @@
1
1
  <script setup lang="ts">
2
2
  // Shared icon-only button primitive that ENFORCES the accessible-name convention (UX-62/63):
3
- // every icon button must carry a `label`, applied as BOTH `title` (pointer tooltip) and
4
- // `aria-label` (screen readers) — `title` alone never fires on touch and is invisible to
5
- // assistive tech, so an unlabeled icon button is a bug this component makes unrepresentable.
6
- // All other UButton props/listeners (icon, color, variant, size, @click, :loading, :disabled)
7
- // pass straight through via `$attrs`; `label` is declared as a prop so it strips off before
8
- // reaching UButton (whose own `label` prop would otherwise render visible text). Mirrors the
9
- // shape of `common/CopyButton.vue`.
3
+ // every icon button must carry a `label`, applied as BOTH the tooltip and `aria-label` (screen
4
+ // readers): an unlabeled icon button is a bug this component makes unrepresentable.
5
+ //
6
+ // The hint is a UTooltip, not a `title` attribute: `title` never fires on touch, is not styled,
7
+ // takes about a second to appear, and is not what the rest of the app's tooltips look like.
8
+ // `aria-label` stays on the button itself, because the tooltip is a hover/focus affordance and
9
+ // the accessible NAME has to be there whether or not one ever opens.
10
+ //
11
+ // All other UButton props/listeners (icon, color, variant, size, `ui`, @click, :loading,
12
+ // :disabled) pass straight through via `$attrs`; `label` is declared as a prop so it strips off
13
+ // before reaching UButton (whose own `label` prop would otherwise render visible text).
14
+ //
15
+ // `class` and `style` land on the WRAPPER, not the button: the wrapper is the box the parent lays
16
+ // out, so `ms-auto`, `shrink-0` or Vue Flow's `nodrag` only work there. The button's own look is
17
+ // its props, and `:ui="{ base: '…' }"` for anything they do not cover.
18
+ import { computed, useAttrs } from 'vue'
19
+
10
20
  defineProps<{
11
- /** Accessible name + tooltip. Required — the whole point of the primitive. */
21
+ /** Accessible name + tooltip. Required: the whole point of the primitive. */
12
22
  label: string
13
23
  }>()
14
24
  defineOptions({ inheritAttrs: false })
25
+
26
+ const attrs = useAttrs()
27
+ const buttonAttrs = computed(() => {
28
+ const { class: _class, style: _style, ...rest } = attrs
29
+ return rest
30
+ })
15
31
  </script>
16
32
 
17
33
  <template>
18
- <UButton v-bind="$attrs" :title="label" :aria-label="label" />
34
+ <UTooltip :text="label">
35
+ <!-- The trigger is a SPAN around the button, not the button. A disabled `<button>` fires no
36
+ pointer or focus event, so a tooltip bound straight to it never opens: the one state
37
+ where the hint is the only thing explaining why the control does nothing (a blocked
38
+ delete naming its reason) was exactly the state that lost it. The span is
39
+ `inline-flex` so it adds no box of its own. -->
40
+ <span class="inline-flex" :class="$attrs.class" :style="$attrs.style">
41
+ <UButton v-bind="buttonAttrs" :aria-label="label" />
42
+ </span>
43
+ </UTooltip>
19
44
  </template>