@cat-factory/app 0.309.0 → 0.310.1

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 +2 -2
@@ -22,6 +22,7 @@ import { connectableSources } from '~/utils/sourcePicker'
22
22
  import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.vue'
23
23
  import ContextIssuePicker from '~/components/tasks/ContextIssuePicker.vue'
24
24
  import SectionLabel from '~/components/common/SectionLabel.vue'
25
+ import IconButton from '~/components/common/IconButton.vue'
25
26
 
26
27
  const props = defineProps<{
27
28
  /** The staged attachments; the host owns the array and commits it after creating the block. */
@@ -289,13 +290,18 @@ function removePending(item: PendingContext) {
289
290
  >
290
291
  {{ t('contextAttachments.importsOnAdd') }}
291
292
  </UBadge>
292
- <button
293
- type="button"
294
- class="ms-auto shrink-0 text-muted hover:text-default"
293
+ <IconButton
294
+ color="neutral"
295
+ variant="ghost"
296
+ icon="i-lucide-x"
297
+ class="ms-auto shrink-0"
298
+ :label="t('contextAttachments.removePending', { name: item.title })"
299
+ :ui="{
300
+ base: 'p-0 text-muted hover:bg-transparent hover:text-default',
301
+ leadingIcon: 'h-3.5 w-3.5',
302
+ }"
295
303
  @click="removePending(item)"
296
- >
297
- <UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
298
- </button>
304
+ />
299
305
  </div>
300
306
  <p
301
307
  v-if="item.unreadable"
@@ -397,13 +403,18 @@ function removePending(item: PendingContext) {
397
403
  >
398
404
  {{ t('contextAttachments.importsOnAdd') }}
399
405
  </UBadge>
400
- <button
401
- type="button"
402
- class="ms-auto shrink-0 text-muted hover:text-default"
406
+ <IconButton
407
+ color="neutral"
408
+ variant="ghost"
409
+ icon="i-lucide-x"
410
+ class="ms-auto shrink-0"
411
+ :label="t('contextAttachments.removePending', { name: item.title })"
412
+ :ui="{
413
+ base: 'p-0 text-muted hover:bg-transparent hover:text-default',
414
+ leadingIcon: 'h-3.5 w-3.5',
415
+ }"
403
416
  @click="removePending(item)"
404
- >
405
- <UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
406
- </button>
417
+ />
407
418
  </div>
408
419
  <!-- An issue reference gets no pre-flight of its own (there is no `parseRef` to ask a
409
420
  tracker), so this line IS its warning: the fetch is attempted when the form opens and
@@ -538,10 +538,11 @@ onMounted(() => {
538
538
 
539
539
  <div class="max-h-56 space-y-0.5 overflow-y-auto">
540
540
  <!-- Already-imported documents (linked directly, no re-fetch). -->
541
- <button
541
+ <UButton
542
+ color="neutral"
543
+ variant="ghost"
542
544
  v-for="d in importedRows"
543
545
  :key="`imp:${d.externalId}`"
544
- type="button"
545
546
  class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-toned hover:bg-elevated/70"
546
547
  @click="pickImported(d.externalId, d.title, d.excerpt)"
547
548
  >
@@ -550,26 +551,28 @@ onMounted(() => {
550
551
  <UBadge color="neutral" variant="soft" size="xs" class="ms-auto shrink-0">{{
551
552
  t('documents.picker.importedBadge')
552
553
  }}</UBadge>
553
- </button>
554
+ </UButton>
554
555
 
555
556
  <!-- Source search hits (imported on add). -->
556
- <button
557
+ <UButton
558
+ color="neutral"
559
+ variant="ghost"
557
560
  v-for="r in searchRows"
558
561
  :key="`hit:${r.externalId}`"
559
- type="button"
560
562
  class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-toned hover:bg-elevated/70"
561
563
  @click="pickSearch(r)"
562
564
  >
563
565
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-muted" />
564
566
  <span class="truncate">{{ r.title }}</span>
565
- </button>
567
+ </UButton>
566
568
 
567
569
  <!-- Explicit URL/ID reference, RESOLVED: the row shows the canonical form the source
568
570
  settled on, so a share link's title segment and tracking params are visibly gone
569
571
  before the attachment is staged. -->
570
- <button
572
+ <UButton
573
+ color="neutral"
574
+ variant="ghost"
571
575
  v-if="refRow"
572
- type="button"
573
576
  class="flex w-full items-start gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-toned hover:bg-elevated/70"
574
577
  data-testid="doc-ref-attach"
575
578
  @click="pickRef(refRow)"
@@ -606,7 +609,7 @@ onMounted(() => {
606
609
  >
607
610
  {{ t('documents.picker.importedBadge') }}
608
611
  </UBadge>
609
- </button>
612
+ </UButton>
610
613
 
611
614
  <EmptyState
612
615
  v-if="empty"
@@ -136,14 +136,15 @@ function preview(externalId: string) {
136
136
  >
137
137
  <div class="flex items-start justify-between gap-2">
138
138
  <div class="min-w-0">
139
- <a
140
- :href="doc.url"
139
+ <ULink
140
+ raw
141
+ :to="doc.url"
141
142
  target="_blank"
142
143
  rel="noopener"
143
144
  class="truncate text-sm font-medium text-highlighted hover:underline"
144
145
  >
145
146
  {{ doc.title }}
146
- </a>
147
+ </ULink>
147
148
  <p class="mt-0.5 line-clamp-2 text-xs text-dimmed">{{ doc.excerpt }}</p>
148
149
  <DocumentSyncState :doc="doc" class="mt-1" />
149
150
  </div>
@@ -16,15 +16,18 @@ const { t } = useI18n()
16
16
  </script>
17
17
 
18
18
  <template>
19
- <component
20
- :is="props.url ? 'a' : 'span'"
21
- :class="props.url ? props.hoverClass : undefined"
22
- v-bind="
23
- props.url
24
- ? { href: props.url, title: props.url, target: '_blank', rel: 'noopener' }
25
- : { title: t('documents.taskDocs.uploadedHint') }
26
- "
19
+ <ULink
20
+ v-if="props.url"
21
+ raw
22
+ :to="props.url"
23
+ target="_blank"
24
+ rel="noopener"
25
+ :title="props.url"
26
+ :class="props.hoverClass"
27
27
  >
28
28
  <slot />
29
- </component>
29
+ </ULink>
30
+ <span v-else :title="t('documents.taskDocs.uploadedHint')">
31
+ <slot />
32
+ </span>
30
33
  </template>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { isConnectableSource } from '@cat-factory/contracts'
3
+ import IconButton from '~/components/common/IconButton.vue'
3
4
  import type { SourceDocument } from '~/types/domain'
4
5
  import {
5
6
  CHANGE_KEYS,
@@ -182,15 +183,14 @@ async function refresh() {
182
183
  <UIcon name="i-lucide-image-off" class="h-3 w-3 shrink-0" />
183
184
  <span class="truncate">{{ renders }}</span>
184
185
  </span>
185
- <UButton
186
+ <IconButton
186
187
  v-if="askable"
187
188
  color="neutral"
188
189
  variant="ghost"
189
190
  size="xs"
190
191
  icon="i-lucide-refresh-cw"
191
192
  :loading="busy"
192
- :aria-label="t('documents.freshness.refresh')"
193
- :title="t('documents.freshness.refresh')"
193
+ :label="t('documents.freshness.refresh')"
194
194
  class="ml-auto shrink-0"
195
195
  @click="refresh"
196
196
  />
@@ -242,16 +242,17 @@ onMounted(() => {
242
242
  {{ t('documents.repoPicker.filesFailed', { error: filesError }) }}
243
243
  </p>
244
244
  <div class="max-h-56 space-y-0.5 overflow-y-auto">
245
- <button
245
+ <UButton
246
+ color="neutral"
247
+ variant="ghost"
246
248
  v-for="f in fileMatches"
247
249
  :key="f.path"
248
- type="button"
249
250
  class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-toned hover:bg-elevated/70"
250
251
  @click="pickFile(f.path)"
251
252
  >
252
253
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-primary" />
253
254
  <span class="truncate">{{ f.path }}</span>
254
- </button>
255
+ </UButton>
255
256
  <p v-if="fileMatchesTruncated" class="px-2 py-1 text-2xs text-dimmed">
256
257
  {{ t('documents.repoPicker.moreFiles', { count: FILE_RESULTS_CAP }) }}
257
258
  </p>
@@ -124,16 +124,17 @@ const envInTransition = computed(
124
124
  ENV_STATUS_META[environment.status].label
125
125
  }}</span>
126
126
  </div>
127
- <a
127
+ <ULink
128
+ raw
128
129
  v-if="environment.url"
129
- :href="environment.url"
130
+ :to="environment.url"
130
131
  target="_blank"
131
132
  rel="noopener"
132
133
  class="inline-flex items-center gap-1.5 break-all text-sm text-app-info-300 hover:underline"
133
134
  >
134
135
  <UIcon name="i-lucide-external-link" class="h-3.5 w-3.5 shrink-0" />
135
136
  {{ environment.url }}
136
- </a>
137
+ </ULink>
137
138
  <p v-if="environment.expiresAt" class="text-2xs text-dimmed">
138
139
  {{ t('environments.expires', { date: d(new Date(environment.expiresAt), 'long') }) }}
139
140
  </p>
@@ -204,14 +204,15 @@ function hasRecordedAnswer(item: FollowUpItem): boolean {
204
204
  {{ item.suggestedAction }}
205
205
  </p>
206
206
  <p v-if="item.status === 'filed' && item.ticketUrl" class="mt-1 text-2xs">
207
- <a
208
- :href="item.ticketUrl"
207
+ <ULink
208
+ raw
209
+ :to="item.ticketUrl"
209
210
  target="_blank"
210
211
  rel="noopener"
211
212
  class="text-app-success-300 hover:underline"
212
213
  >
213
214
  {{ item.ticketExternalId ?? t('followUp.viewIssue') }}
214
- </a>
215
+ </ULink>
215
216
  </p>
216
217
  <p v-if="hasRecordedAnswer(item)" class="mt-1 text-2xs text-toned">
217
218
  <span class="text-dimmed">
@@ -231,11 +232,13 @@ function hasRecordedAnswer(item: FollowUpItem): boolean {
231
232
  <div v-if="item.status === 'pending'" class="mt-2.5">
232
233
  <!-- A question: answer it -->
233
234
  <div v-if="item.kind === 'question'" class="space-y-2">
234
- <textarea
235
+ <UTextarea
235
236
  v-model="drafts[item.id]"
236
- rows="2"
237
+ :rows="2"
237
238
  :placeholder="t('followUp.answerPlaceholder')"
238
- class="w-full resize-y rounded-md border border-muted bg-app-950/60 px-2.5 py-1.5 text-xs text-app-100 placeholder:text-app-600 focus:border-app-info-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-app-info-500/60"
239
+ size="xs"
240
+ class="w-full"
241
+ :ui="{ base: 'resize-y' }"
239
242
  />
240
243
  <!-- Wraps, like the follow-up row below: three buttons whose labels are two
241
244
  words each in English are one long line in most of the other locales, and
@@ -64,6 +64,12 @@ const canChat = computed(() => awaiting.value && !chatBudgetSpent.value && !fork
64
64
 
65
65
  // The human's selection: a proposed fork id, or the sentinel 'custom' for the free-text path.
66
66
  const selected = ref<string | null>(null)
67
+ // The proposed forks and "my own approach" are one choice, so they are one item list. `fork` is
68
+ // null on the custom entry, which is what the label and description slots branch on.
69
+ const forkItems = computed(() => [
70
+ ...forks.value.map((fork) => ({ value: fork.id, label: fork.title, fork })),
71
+ { value: 'custom', label: t('forkDecision.custom.title'), fork: null as ForkOption | null },
72
+ ])
67
73
  const customText = ref('')
68
74
  const note = ref('')
69
75
  const chatInput = ref('')
@@ -217,92 +223,75 @@ const { requestClose } = useUnsavedGuard({
217
223
  {{ state.seamSummary }}
218
224
  </p>
219
225
 
220
- <!-- Proposed fork cards -->
221
- <article
222
- v-for="fork in forks"
223
- :key="fork.id"
224
- data-testid="fork-option-card"
225
- class="cursor-pointer rounded-xl border px-4 py-3 transition"
226
- :class="
227
- selected === fork.id
228
- ? 'border-app-secondary-500/70 bg-app-secondary-500/5'
229
- : 'border-default bg-default/60 hover:border-muted'
230
- "
231
- @click="selected = fork.id"
226
+ <!-- The proposed forks and "my own approach" are ONE choice, so they are one radio
227
+ group rather than a set of cards each holding their own radio. `variant="card"`
228
+ is the shape these already had; the rich body rides the description slot. -->
229
+ <URadioGroup
230
+ :model-value="selected ?? undefined"
231
+ color="secondary"
232
+ variant="card"
233
+ :items="forkItems"
234
+ data-testid="fork-options"
235
+ :ui="{ item: 'px-4 py-3' }"
236
+ @update:model-value="selected = String($event)"
232
237
  >
233
- <div class="flex items-start gap-2">
234
- <input
235
- type="radio"
236
- class="mt-1 accent-app-secondary-500"
237
- :checked="selected === fork.id"
238
- @change="selected = fork.id"
239
- />
240
- <div class="min-w-0 flex-1">
241
- <div class="flex items-center gap-2">
242
- <h3 class="min-w-0 flex-1 text-sm font-medium text-app-100">
243
- {{ fork.title }}
244
- </h3>
245
- <UBadge v-if="fork.recommended" color="primary" variant="subtle" size="sm">
246
- {{ t('forkDecision.recommended') }}
247
- </UBadge>
248
- </div>
249
- <p v-if="fork.summary" class="mt-0.5 text-xs text-muted">
250
- {{ fork.summary }}
251
- </p>
252
- <p class="mt-1.5 whitespace-pre-wrap text-xs text-toned">
253
- {{ fork.approach }}
254
- </p>
255
- <ul v-if="fork.tradeoffs.length" class="mt-1.5 space-y-0.5">
256
- <li
257
- v-for="(tr, i) in fork.tradeoffs"
238
+ <template #label="{ item }">
239
+ <span class="flex items-center gap-2">
240
+ <span class="min-w-0 flex-1 text-sm font-medium text-app-100">{{ item.label }}</span>
241
+ <UBadge v-if="item.fork?.recommended" color="primary" variant="subtle" size="sm">
242
+ {{ t('forkDecision.recommended') }}
243
+ </UBadge>
244
+ </span>
245
+ </template>
246
+ <template #description="{ item }">
247
+ <template v-if="item.fork">
248
+ <span v-if="item.fork.summary" class="block text-xs text-muted">
249
+ {{ item.fork.summary }}
250
+ </span>
251
+ <span class="mt-1.5 block whitespace-pre-wrap text-xs text-toned">
252
+ {{ item.fork.approach }}
253
+ </span>
254
+ <span v-if="item.fork.tradeoffs.length" class="mt-1.5 block space-y-0.5">
255
+ <span
256
+ v-for="(tr, i) in item.fork.tradeoffs"
258
257
  :key="i"
259
258
  class="flex gap-1.5 text-2xs text-muted"
260
259
  >
261
260
  <span class="text-app-600">•</span>{{ tr }}
262
- </li>
263
- </ul>
264
- <p v-if="fork.riskNotes" class="mt-1.5 text-2xs text-app-warning-300/90">
261
+ </span>
262
+ </span>
263
+ <span
264
+ v-if="item.fork.riskNotes"
265
+ class="mt-1.5 block text-2xs text-app-warning-300/90"
266
+ >
265
267
  <span class="text-app-warning-500/70">{{ t('forkDecision.riskNotes') }}</span>
266
- {{ fork.riskNotes }}
267
- </p>
268
- </div>
269
- </div>
270
- </article>
271
-
272
- <!-- Custom approach -->
273
- <article
274
- class="rounded-xl border px-4 py-3 transition"
275
- :class="
276
- selected === 'custom'
277
- ? 'border-app-secondary-500/70 bg-app-secondary-500/5'
278
- : 'border-default bg-default/60'
279
- "
280
- >
281
- <label class="flex cursor-pointer items-center gap-2" @click="selected = 'custom'">
282
- <input type="radio" class="accent-app-secondary-500" :checked="selected === 'custom'" />
283
- <span class="text-sm font-medium text-app-100">{{
284
- t('forkDecision.custom.title')
285
- }}</span>
286
- </label>
287
- <textarea
288
- v-model="customText"
289
- data-testid="fork-custom-input"
290
- rows="3"
291
- :placeholder="t('forkDecision.custom.placeholder')"
292
- class="mt-2 w-full resize-y rounded-md border border-muted bg-app-950/60 px-2.5 py-1.5 text-xs text-app-100 placeholder:text-app-600 focus:border-app-secondary-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-app-secondary-500/60"
293
- @focus="selected = 'custom'"
294
- />
295
- </article>
268
+ {{ item.fork.riskNotes }}
269
+ </span>
270
+ </template>
271
+ <UTextarea
272
+ v-else
273
+ v-model="customText"
274
+ data-testid="fork-custom-input"
275
+ :rows="3"
276
+ size="xs"
277
+ class="mt-2 w-full"
278
+ :ui="{ base: 'resize-y' }"
279
+ :placeholder="t('forkDecision.custom.placeholder')"
280
+ @focus="selected = 'custom'"
281
+ />
282
+ </template>
283
+ </URadioGroup>
296
284
 
297
285
  <!-- Optional steering note -->
298
286
  <div>
299
- <label class="mb-1 block text-2xs text-muted">{{ t('forkDecision.noteLabel') }}</label>
300
- <input
301
- v-model="note"
302
- type="text"
303
- :placeholder="t('forkDecision.notePlaceholder')"
304
- class="w-full rounded-md border border-muted bg-app-950/60 px-2.5 py-1.5 text-xs text-app-100 placeholder:text-app-600 focus:border-app-secondary-500 focus:outline-none"
305
- />
287
+ <UFormField size="xs" :label="t('forkDecision.noteLabel')">
288
+ <UInput
289
+ v-model="note"
290
+ size="xs"
291
+ class="w-full"
292
+ :placeholder="t('forkDecision.notePlaceholder')"
293
+ />
294
+ </UFormField>
306
295
  </div>
307
296
 
308
297
  <!-- Grounded chat: ask about the forks before deciding. -->
@@ -342,18 +331,20 @@ const { requestClose } = useUnsavedGuard({
342
331
  {{ t('forkDecision.chat.hint') }}
343
332
  </p>
344
333
  <div class="mt-2 flex items-end gap-2">
345
- <textarea
334
+ <UTextarea
346
335
  v-model="chatInput"
347
336
  data-testid="fork-chat-input"
348
- rows="2"
337
+ :rows="2"
349
338
  :disabled="!canChat"
350
339
  :placeholder="
351
340
  chatBudgetSpent
352
341
  ? t('forkDecision.chat.budgetSpent')
353
342
  : t('forkDecision.chat.placeholder')
354
343
  "
355
- class="min-h-0 flex-1 resize-y rounded-md border border-muted bg-app-950/60 px-2.5 py-1.5 text-xs text-app-100 placeholder:text-app-600 focus:border-app-secondary-500 focus:outline-none disabled:opacity-50"
356
344
  @keydown.enter.exact.prevent="onSend"
345
+ size="xs"
346
+ class="min-h-0 flex-1"
347
+ :ui="{ base: 'resize-y' }"
357
348
  />
358
349
  <UButton
359
350
  data-testid="fork-chat-send"
@@ -19,6 +19,7 @@ import { useBusyRows } from '~/composables/useBusyRows'
19
19
  import type { ApiContractFormat, FoundationalServiceTier } from '~/types/domain'
20
20
  import { useFoundationalServicesStore } from '~/stores/foundationalServices'
21
21
  import FoundationalContractSummary from '~/components/foundational/FoundationalContractSummary.vue'
22
+ import IconButton from '~/components/common/IconButton.vue'
22
23
 
23
24
  const catalog = useFoundationalServicesStore()
24
25
  const toast = useToast()
@@ -139,13 +140,13 @@ async function suppress(serviceId: string) {
139
140
  <!-- Only an INHERITED entry can be suppressed — an account service or one the
140
141
  deployment registered in code; the board's own row is managed in the registry tab,
141
142
  where deleting it is the honest action. -->
142
- <UButton
143
+ <IconButton
143
144
  v-if="s.tier !== 'workspace'"
144
145
  icon="i-lucide-eye-off"
145
146
  size="xs"
146
147
  variant="ghost"
147
148
  :loading="rowBusy(`suppress:${s.id}`)"
148
- :title="t('foundational.catalog.suppress')"
149
+ :label="t('foundational.catalog.suppress')"
149
150
  :data-testid="`foundational-suppress-${s.id}`"
150
151
  @click="suppress(s.id)"
151
152
  />
@@ -20,6 +20,7 @@ import {
20
20
  useFoundationalServicesStore,
21
21
  } from '~/stores/foundationalServices'
22
22
  import FoundationalContractSummary from '~/components/foundational/FoundationalContractSummary.vue'
23
+ import IconButton from '~/components/common/IconButton.vue'
23
24
 
24
25
  const props = defineProps<{ kind: FoundationalServiceOwnerKind; ownerId: string }>()
25
26
 
@@ -203,20 +204,20 @@ async function remove(service: FoundationalService) {
203
204
  </p>
204
205
  </div>
205
206
  <div class="flex shrink-0 gap-1">
206
- <UButton
207
+ <IconButton
207
208
  v-if="!s.sourceId"
208
209
  icon="i-lucide-pencil"
209
210
  size="xs"
210
211
  variant="ghost"
211
- :title="t('foundational.registry.edit')"
212
+ :label="t('foundational.registry.edit')"
212
213
  @click="openEdit(s)"
213
214
  />
214
- <UButton
215
+ <IconButton
215
216
  icon="i-lucide-trash-2"
216
217
  size="xs"
217
218
  color="error"
218
219
  variant="ghost"
219
- :title="t('foundational.registry.delete')"
220
+ :label="t('foundational.registry.delete')"
220
221
  @click="remove(s)"
221
222
  />
222
223
  </div>
@@ -289,12 +290,12 @@ async function remove(service: FoundationalService) {
289
290
  class="flex-1"
290
291
  />
291
292
  <USelect v-model="c.format" :items="formatItems" class="w-56" />
292
- <UButton
293
+ <IconButton
293
294
  icon="i-lucide-x"
294
295
  size="xs"
295
296
  color="error"
296
297
  variant="ghost"
297
- :title="t('foundational.registry.removeContract')"
298
+ :label="t('foundational.registry.removeContract')"
298
299
  @click="removeContract(i)"
299
300
  />
300
301
  </div>
@@ -27,6 +27,7 @@ import {
27
27
  } from '~/stores/foundationalServices'
28
28
  import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.vue'
29
29
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
30
+ import IconButton from '~/components/common/IconButton.vue'
30
31
 
31
32
  const props = defineProps<{ kind: FoundationalServiceOwnerKind; ownerId: string }>()
32
33
 
@@ -295,29 +296,29 @@ async function unlink(id: string) {
295
296
  {{ t('foundational.sources.changes') }}
296
297
  </UBadge>
297
298
  <div class="ms-auto flex gap-1">
298
- <UButton
299
+ <IconButton
299
300
  icon="i-lucide-search-check"
300
301
  size="xs"
301
302
  variant="ghost"
302
303
  :loading="rowBusy(`check:${s.id}`)"
303
- :title="t('foundational.sources.check')"
304
+ :label="t('foundational.sources.check')"
304
305
  @click="check(s.id)"
305
306
  />
306
- <UButton
307
+ <IconButton
307
308
  icon="i-lucide-refresh-cw"
308
309
  size="xs"
309
310
  variant="ghost"
310
311
  :loading="rowBusy(`sync:${s.id}`)"
311
- :title="t('foundational.sources.sync')"
312
+ :label="t('foundational.sources.sync')"
312
313
  @click="sync(s.id)"
313
314
  />
314
- <UButton
315
+ <IconButton
315
316
  icon="i-lucide-unplug"
316
317
  size="xs"
317
318
  color="error"
318
319
  variant="ghost"
319
320
  :loading="rowBusy(`unlink:${s.id}`)"
320
- :title="t('foundational.sources.unlink')"
321
+ :label="t('foundational.sources.unlink')"
321
322
  @click="unlink(s.id)"
322
323
  />
323
324
  </div>
@@ -19,6 +19,7 @@ import { showOverrideField } from '~/utils/uiMode'
19
19
  import GitHubRepoSearchSelect from '~/components/github/GitHubRepoSearchSelect.vue'
20
20
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
21
21
  import GitHubDocUrlImport from '~/components/fragments/GitHubDocUrlImport.vue'
22
+ import IconButton from '~/components/common/IconButton.vue'
22
23
 
23
24
  const props = withDefaults(
24
25
  defineProps<{
@@ -750,12 +751,12 @@ async function unlinkSource(id: string) {
750
751
  <div class="ms-auto flex gap-1">
751
752
  <!-- Editing a repo-SOURCED fragment locally would be overwritten on the next sync,
752
753
  so only hand-authored fragments are editable here. -->
753
- <UButton
754
+ <IconButton
754
755
  v-if="!f.source"
755
756
  icon="i-lucide-pencil"
756
757
  size="xs"
757
758
  variant="ghost"
758
- :title="t('common.edit')"
759
+ :label="t('common.edit')"
759
760
  @click="startEdit(f)"
760
761
  />
761
762
  <UButton
@@ -866,12 +867,12 @@ async function unlinkSource(id: string) {
866
867
  </p>
867
868
  </div>
868
869
  <div class="ms-auto flex gap-1">
869
- <UButton
870
+ <IconButton
870
871
  icon="i-lucide-refresh-cw"
871
872
  size="xs"
872
873
  variant="ghost"
873
874
  :loading="rowBusy(`refresh:${f.id}`)"
874
- :title="t('fragments.documents.refreshTitle')"
875
+ :label="t('fragments.documents.refreshTitle')"
875
876
  @click="refreshFragment(f.id)"
876
877
  />
877
878
  <UButton