@cat-factory/app 0.47.8 → 0.47.10

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.
@@ -11,6 +11,8 @@ const ui = useUiStore()
11
11
  const models = useModelsStore()
12
12
  const workspace = useWorkspaceStore()
13
13
 
14
+ const { t, n } = useI18n()
15
+
14
16
  onMounted(() => models.ensureLoaded(workspace.workspaceId ?? undefined))
15
17
 
16
18
  const block = computed<Block | undefined>(() =>
@@ -64,7 +66,7 @@ function openApprovalFor(approvalId: string) {
64
66
  size="sm"
65
67
  @click="close"
66
68
  >
67
- Board
69
+ {{ t('focus.board') }}
68
70
  </UButton>
69
71
  <UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600" />
70
72
  <div
@@ -75,7 +77,9 @@ function openApprovalFor(approvalId: string) {
75
77
  </div>
76
78
  <div>
77
79
  <h1 class="text-lg font-semibold text-white">{{ block.title }}</h1>
78
- <div class="text-xs text-slate-500">{{ typeMeta.label }} · focus view</div>
80
+ <div class="text-xs text-slate-500">
81
+ {{ t('focus.typeSubtitle', { type: typeMeta.label }) }}
82
+ </div>
79
83
  </div>
80
84
  <UBadge :color="statusMeta.chip as any" variant="subtle" class="ml-2">
81
85
  {{ statusMeta.label }}
@@ -89,7 +93,7 @@ function openApprovalFor(approvalId: string) {
89
93
  icon="i-lucide-play"
90
94
  trailing-icon="i-lucide-chevron-down"
91
95
  >
92
- {{ instance ? 'Re-run pipeline' : 'Run pipeline' }}
96
+ {{ instance ? t('focus.rerunPipeline') : t('focus.runPipeline') }}
93
97
  </UButton>
94
98
  </UDropdownMenu>
95
99
  <UButton icon="i-lucide-x" color="neutral" variant="ghost" @click="close" />
@@ -104,7 +108,7 @@ function openApprovalFor(approvalId: string) {
104
108
  <div class="mb-4 flex items-center gap-2">
105
109
  <UIcon name="i-lucide-workflow" class="h-4 w-4 text-slate-500" />
106
110
  <h2 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
107
- {{ instance ? instance.pipelineName : 'No pipeline running' }}
111
+ {{ instance ? instance.pipelineName : t('focus.noPipelineRunning') }}
108
112
  </h2>
109
113
  </div>
110
114
 
@@ -119,7 +123,7 @@ function openApprovalFor(approvalId: string) {
119
123
  v-else
120
124
  class="flex flex-1 items-center justify-center rounded-xl border border-dashed border-slate-700 text-sm text-slate-500"
121
125
  >
122
- Run a pipeline to visualize the agents working on this block.
126
+ {{ t('focus.emptyPipelineHint') }}
123
127
  </div>
124
128
  </section>
125
129
 
@@ -129,29 +133,29 @@ function openApprovalFor(approvalId: string) {
129
133
  >
130
134
  <div>
131
135
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
132
- Description
136
+ {{ t('focus.description') }}
133
137
  </div>
134
138
  <p class="text-sm text-slate-300">{{ block.description }}</p>
135
139
  </div>
136
140
  <div v-if="instance">
137
141
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
138
- Overall progress
142
+ {{ t('focus.overallProgress') }}
139
143
  </div>
140
144
  <UProgress :model-value="Math.round(block.progress * 100)" />
141
145
  <div class="mt-1 text-[11px] text-slate-400">
142
- {{ Math.round(block.progress * 100) }}%
146
+ {{ n(block.progress, 'percent') }}
143
147
  </div>
144
148
  </div>
145
149
  <div>
146
150
  <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
147
- Dependencies
151
+ {{ t('focus.dependencies') }}
148
152
  </div>
149
153
  <div v-if="deps.length" class="flex flex-wrap gap-1">
150
154
  <UBadge v-for="d in deps" :key="d.id" color="neutral" variant="subtle" size="sm">
151
155
  {{ d.title }}
152
156
  </UBadge>
153
157
  </div>
154
- <div v-else class="text-[11px] text-slate-500">None</div>
158
+ <div v-else class="text-[11px] text-slate-500">{{ t('focus.noDependencies') }}</div>
155
159
  </div>
156
160
  </aside>
157
161
  </div>
@@ -17,6 +17,8 @@ const execution = useExecutionStore()
17
17
  const board = useBoardStore()
18
18
  const followUps = useFollowUpsStore()
19
19
 
20
+ const { t } = useI18n()
21
+
20
22
  const { open, blockId, instanceId, stepIndex, close } = useResultView('follow-ups')
21
23
 
22
24
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
@@ -57,15 +59,25 @@ async function onDismiss(item: FollowUpItem) {
57
59
  if (id) await followUps.dismissItem(id, item.id).catch(() => {})
58
60
  }
59
61
 
62
+ // Exhaustive map of the item status enum → label key (literal keys keep the typed-key
63
+ // drift guard live, vs a runtime-built `followUp.status.${status}`).
64
+ const STATUS_LABEL_KEYS: Record<FollowUpItem['status'], string> = {
65
+ pending: 'followUp.status.pending',
66
+ filed: 'followUp.status.filed',
67
+ queued: 'followUp.status.queued',
68
+ answered: 'followUp.status.answered',
69
+ dismissed: 'followUp.status.dismissed',
70
+ }
71
+
60
72
  const STATUS_META: Record<
61
73
  FollowUpItem['status'],
62
- { label: string; badge: 'neutral' | 'info' | 'success' | 'warning'; text: string }
74
+ { badge: 'neutral' | 'info' | 'success' | 'warning'; text: string }
63
75
  > = {
64
- pending: { label: 'Needs a decision', badge: 'warning', text: 'text-amber-300' },
65
- filed: { label: 'Filed as issue', badge: 'success', text: 'text-emerald-300' },
66
- queued: { label: 'Sent to Coder', badge: 'info', text: 'text-sky-300' },
67
- answered: { label: 'Answered', badge: 'info', text: 'text-sky-300' },
68
- dismissed: { label: 'Dismissed', badge: 'neutral', text: 'text-slate-400' },
76
+ pending: { badge: 'warning', text: 'text-amber-300' },
77
+ filed: { badge: 'success', text: 'text-emerald-300' },
78
+ queued: { badge: 'info', text: 'text-sky-300' },
79
+ answered: { badge: 'info', text: 'text-sky-300' },
80
+ dismissed: { badge: 'neutral', text: 'text-slate-400' },
69
81
  }
70
82
  </script>
71
83
 
@@ -88,15 +100,20 @@ const STATUS_META: Record<
88
100
  </span>
89
101
  <div class="min-w-0 flex-1">
90
102
  <h2 class="truncate text-sm font-semibold text-slate-100">
91
- {{ FOLLOW_UP_COMPANION_META.label }}{{ block ? ` — ${block.title}` : '' }}
103
+ {{
104
+ block ? t('followUp.titleWithBlock', { title: block.title }) : t('followUp.title')
105
+ }}
92
106
  </h2>
93
107
  <p class="truncate text-[11px] text-slate-400">
94
- Forward-looking follow-ups & questions the Coder surfaced. The pipeline continues once
95
- every item is decided.
108
+ {{ t('followUp.subtitle') }}
96
109
  </p>
97
110
  </div>
98
111
  <UBadge :color="pendingCount > 0 ? 'warning' : 'success'" variant="subtle" size="sm">
99
- {{ pendingCount > 0 ? `${pendingCount} to decide` : 'All decided' }}
112
+ {{
113
+ pendingCount > 0
114
+ ? t('followUp.badge.toDecide', { count: pendingCount }, pendingCount)
115
+ : t('followUp.badge.allDecided')
116
+ }}
100
117
  </UBadge>
101
118
  <button
102
119
  class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
@@ -113,10 +130,9 @@ const STATUS_META: Record<
113
130
  class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
114
131
  >
115
132
  <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-8 w-8 opacity-40" />
116
- <p class="text-sm">No follow-ups yet.</p>
133
+ <p class="text-sm">{{ t('followUp.empty.title') }}</p>
117
134
  <p class="max-w-sm text-[11px] text-slate-500">
118
- As the Coder works it streams loose ends, side-tasks and questions here. They appear
119
- live — you can act on them before the Coder even finishes.
135
+ {{ t('followUp.empty.hint') }}
120
136
  </p>
121
137
  </div>
122
138
 
@@ -146,14 +162,15 @@ const STATUS_META: Record<
146
162
  {{ item.title }}
147
163
  </h3>
148
164
  <UBadge :color="STATUS_META[item.status].badge" variant="subtle" size="sm">
149
- {{ STATUS_META[item.status].label }}
165
+ {{ t(STATUS_LABEL_KEYS[item.status]) }}
150
166
  </UBadge>
151
167
  </div>
152
168
  <p v-if="item.detail" class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
153
169
  {{ item.detail }}
154
170
  </p>
155
171
  <p v-if="item.suggestedAction" class="mt-1 text-[11px] text-slate-400">
156
- <span class="text-slate-500">Suggested:</span> {{ item.suggestedAction }}
172
+ <span class="text-slate-500">{{ t('followUp.suggested') }}</span>
173
+ {{ item.suggestedAction }}
157
174
  </p>
158
175
  <p v-if="item.status === 'filed' && item.ticketUrl" class="mt-1 text-[11px]">
159
176
  <a
@@ -162,14 +179,15 @@ const STATUS_META: Record<
162
179
  rel="noopener"
163
180
  class="text-emerald-300 hover:underline"
164
181
  >
165
- {{ item.ticketExternalId ?? 'View issue' }}
182
+ {{ item.ticketExternalId ?? t('followUp.viewIssue') }}
166
183
  </a>
167
184
  </p>
168
185
  <p
169
186
  v-if="item.status === 'answered' && item.answer"
170
187
  class="mt-1 text-[11px] text-slate-300"
171
188
  >
172
- <span class="text-slate-500">Your answer:</span> {{ item.answer }}
189
+ <span class="text-slate-500">{{ t('followUp.yourAnswer') }}</span>
190
+ {{ item.answer }}
173
191
  </p>
174
192
 
175
193
  <!-- Actions (only while the item is still undecided) -->
@@ -179,7 +197,7 @@ const STATUS_META: Record<
179
197
  <textarea
180
198
  v-model="drafts[item.id]"
181
199
  rows="2"
182
- placeholder="Answer this question — it's folded into the Coder's next pass…"
200
+ :placeholder="t('followUp.answerPlaceholder')"
183
201
  class="w-full resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-sky-500 focus:outline-none"
184
202
  />
185
203
  <div class="flex items-center gap-2">
@@ -190,7 +208,7 @@ const STATUS_META: Record<
190
208
  :disabled="!(drafts[item.id] ?? '').trim()"
191
209
  @click="onAnswer(item)"
192
210
  >
193
- Answer & send back
211
+ {{ t('followUp.actions.answerAndSend') }}
194
212
  </UButton>
195
213
  <UButton
196
214
  size="xs"
@@ -199,7 +217,7 @@ const STATUS_META: Record<
199
217
  :loading="followUps.isActing(item.id)"
200
218
  @click="onDismiss(item)"
201
219
  >
202
- Dismiss
220
+ {{ t('followUp.actions.dismiss') }}
203
221
  </UButton>
204
222
  </div>
205
223
  </div>
@@ -213,7 +231,7 @@ const STATUS_META: Record<
213
231
  :loading="followUps.isActing(item.id)"
214
232
  @click="onFile(item)"
215
233
  >
216
- File as issue
234
+ {{ t('followUp.actions.fileAsIssue') }}
217
235
  </UButton>
218
236
  <UButton
219
237
  size="xs"
@@ -223,7 +241,7 @@ const STATUS_META: Record<
223
241
  :loading="followUps.isActing(item.id)"
224
242
  @click="onQueue(item)"
225
243
  >
226
- Send to Coder
244
+ {{ t('followUp.actions.sendToCoder') }}
227
245
  </UButton>
228
246
  <UButton
229
247
  size="xs"
@@ -232,7 +250,7 @@ const STATUS_META: Record<
232
250
  :loading="followUps.isActing(item.id)"
233
251
  @click="onDismiss(item)"
234
252
  >
235
- Dismiss
253
+ {{ t('followUp.actions.dismiss') }}
236
254
  </UButton>
237
255
  </div>
238
256
  </div>
@@ -246,10 +264,17 @@ const STATUS_META: Record<
246
264
  class="flex items-center justify-between border-t border-slate-800 px-5 py-2.5 text-[11px] text-slate-400"
247
265
  >
248
266
  <span>
249
- {{ items.length }} item{{ items.length === 1 ? '' : 's' }} ·
250
- {{ pendingCount }} undecided
267
+ {{
268
+ t(
269
+ 'followUp.footer.summary',
270
+ { count: items.length, undecided: pendingCount },
271
+ items.length,
272
+ )
273
+ }}
251
274
  </span>
252
- <span v-if="maxLoops > 0">Coder loops used: {{ loops }} / {{ maxLoops }}</span>
275
+ <span v-if="maxLoops > 0">{{
276
+ t('followUp.footer.loops', { loops, max: maxLoops })
277
+ }}</span>
253
278
  </footer>
254
279
  </div>
255
280
  </div>
@@ -5,12 +5,20 @@
5
5
  // stream), surfaces the ephemeral environment URL, and drives the human actions: confirm
6
6
  // (pass + tear down + advance), request a fix from findings (the Tester's fixer), pull latest
7
7
  // main into the branch + redeploy (conflict → conflict-resolver), recreate, or destroy the env.
8
- import type { HumanTestEnvironmentStatus, HumanTestStepState } from '~/types/execution'
8
+ import type {
9
+ HumanTestEnvironmentStatus,
10
+ HumanTestRound,
11
+ HumanTestStepState,
12
+ } from '~/types/execution'
13
+
14
+ // The round outcome union (contracts state it inline, so derive it off the round type).
15
+ type HumanTestRoundOutcome = NonNullable<HumanTestRound['outcome']>
9
16
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
10
17
 
11
18
  const board = useBoardStore()
12
19
  const execution = useExecutionStore()
13
20
  const humanTest = useHumanTestStore()
21
+ const { t, d } = useI18n()
14
22
 
15
23
  // Shared seam contract (open/blockId/close + Escape). No `onOpen` loader: the gate state
16
24
  // rides on the execution step, pushed over the stream.
@@ -38,21 +46,36 @@ const working = computed(
38
46
  phase.value === 'resolving_conflicts',
39
47
  )
40
48
 
41
- const ENV_STATUS_META: Record<HumanTestEnvironmentStatus, { label: string; color: string }> = {
42
- provisioning: { label: 'Provisioning…', color: 'text-amber-300' },
43
- ready: { label: 'Ready', color: 'text-emerald-300' },
44
- failed: { label: 'Failed', color: 'text-rose-300' },
45
- expired: { label: 'Expired', color: 'text-slate-400' },
46
- tearing_down: { label: 'Tearing down…', color: 'text-slate-400' },
47
- torn_down: { label: 'Destroyed', color: 'text-slate-400' },
49
+ // Exhaustive enum→label maps of literal keys (keeps the typed-key drift guard live vs a
50
+ // runtime-built `humanTest.envStatus.${status}`). The colour stays a component constant.
51
+ const ENV_STATUS_LABEL: Record<HumanTestEnvironmentStatus, string> = {
52
+ provisioning: 'humanTest.envStatus.provisioning',
53
+ ready: 'humanTest.envStatus.ready',
54
+ failed: 'humanTest.envStatus.failed',
55
+ expired: 'humanTest.envStatus.expired',
56
+ tearing_down: 'humanTest.envStatus.tearingDown',
57
+ torn_down: 'humanTest.envStatus.tornDown',
58
+ }
59
+ const ENV_STATUS_COLOR: Record<HumanTestEnvironmentStatus, string> = {
60
+ provisioning: 'text-amber-300',
61
+ ready: 'text-emerald-300',
62
+ failed: 'text-rose-300',
63
+ expired: 'text-slate-400',
64
+ tearing_down: 'text-slate-400',
65
+ torn_down: 'text-slate-400',
48
66
  }
49
67
 
50
68
  const PHASE_LABEL: Record<NonNullable<HumanTestStepState['phase']>, string> = {
51
- provisioning: 'Provisioning environment…',
52
- awaiting_human: 'Awaiting your validation',
53
- fixing: 'Fixer is addressing your findings…',
54
- resolving_conflicts: 'Resolving conflicts with main…',
55
- passed: 'Passed',
69
+ provisioning: 'humanTest.phase.provisioning',
70
+ awaiting_human: 'humanTest.phase.awaitingHuman',
71
+ fixing: 'humanTest.phase.fixing',
72
+ resolving_conflicts: 'humanTest.phase.resolvingConflicts',
73
+ passed: 'humanTest.phase.passed',
74
+ }
75
+
76
+ const ROUND_OUTCOME_LABEL: Record<HumanTestRoundOutcome, string> = {
77
+ completed: 'humanTest.roundOutcome.completed',
78
+ failed: 'humanTest.roundOutcome.failed',
56
79
  }
57
80
 
58
81
  const findings = ref('')
@@ -115,10 +138,12 @@ const canDestroy = computed(
115
138
  </span>
116
139
  <div class="min-w-0 flex-1">
117
140
  <h2 class="truncate text-sm font-semibold text-slate-100">
118
- Human testing{{ block ? ` — ${block.title}` : '' }}
141
+ {{
142
+ block ? t('humanTest.titleWithBlock', { title: block.title }) : t('humanTest.title')
143
+ }}
119
144
  </h2>
120
145
  <p class="truncate text-[11px] text-slate-400">
121
- {{ phase ? PHASE_LABEL[phase] : 'Validate the change in a live environment' }}
146
+ {{ phase ? t(PHASE_LABEL[phase]) : t('humanTest.subtitle') }}
122
147
  </p>
123
148
  </div>
124
149
  <button
@@ -135,24 +160,24 @@ const canDestroy = computed(
135
160
  class="flex flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
136
161
  >
137
162
  <UIcon name="i-lucide-user-check" class="h-8 w-8 opacity-40" />
138
- <p class="text-sm">This step hasn't started yet.</p>
163
+ <p class="text-sm">{{ t('humanTest.notStarted') }}</p>
139
164
  </div>
140
165
 
141
166
  <template v-else>
142
167
  <!-- Environment -->
143
168
  <section class="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
144
169
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
145
- Ephemeral environment
170
+ {{ t('humanTest.environment.heading') }}
146
171
  </h3>
147
172
  <div v-if="env" class="space-y-2">
148
173
  <div class="flex items-center gap-2 text-[13px]">
149
174
  <UIcon
150
175
  name="i-lucide-circle-dot"
151
176
  class="h-3.5 w-3.5"
152
- :class="ENV_STATUS_META[env.status].color"
177
+ :class="ENV_STATUS_COLOR[env.status]"
153
178
  />
154
- <span :class="ENV_STATUS_META[env.status].color">{{
155
- ENV_STATUS_META[env.status].label
179
+ <span :class="ENV_STATUS_COLOR[env.status]">{{
180
+ t(ENV_STATUS_LABEL[env.status])
156
181
  }}</span>
157
182
  </div>
158
183
  <a
@@ -165,13 +190,17 @@ const canDestroy = computed(
165
190
  <UIcon name="i-lucide-external-link" class="h-3.5 w-3.5 shrink-0" />
166
191
  {{ env.url }}
167
192
  </a>
168
- <p v-else class="text-[12px] italic text-slate-500">No URL yet.</p>
193
+ <p v-else class="text-[12px] italic text-slate-500">
194
+ {{ t('humanTest.environment.noUrl') }}
195
+ </p>
169
196
  <p v-if="env.expiresAt" class="text-[11px] text-slate-500">
170
- Expires {{ new Date(env.expiresAt).toLocaleString() }}
197
+ {{
198
+ t('humanTest.environment.expires', { date: d(new Date(env.expiresAt), 'long') })
199
+ }}
171
200
  </p>
172
201
  </div>
173
202
  <p v-else class="text-[12px] text-amber-300/90">
174
- {{ ht.degradedReason ?? 'No live environment.' }}
203
+ {{ ht.degradedReason ?? t('humanTest.environment.none') }}
175
204
  </p>
176
205
  <p v-if="env && ht.degradedReason" class="mt-2 text-[12px] text-amber-300/90">
177
206
  {{ ht.degradedReason }}
@@ -188,7 +217,7 @@ const canDestroy = computed(
188
217
  :disabled="busy || !canManageEnv"
189
218
  @click="recreate"
190
219
  >
191
- Recreate
220
+ {{ t('humanTest.actions.recreate') }}
192
221
  </UButton>
193
222
  <UButton
194
223
  size="xs"
@@ -198,7 +227,7 @@ const canDestroy = computed(
198
227
  :disabled="busy || !canDestroy"
199
228
  @click="destroy"
200
229
  >
201
- Destroy
230
+ {{ t('humanTest.actions.destroy') }}
202
231
  </UButton>
203
232
  <UButton
204
233
  size="xs"
@@ -209,7 +238,7 @@ const canDestroy = computed(
209
238
  :disabled="busy || !canManageEnv"
210
239
  @click="pullMain"
211
240
  >
212
- Pull main + redeploy
241
+ {{ t('humanTest.actions.pullMain') }}
213
242
  </UButton>
214
243
  </div>
215
244
  </section>
@@ -220,7 +249,7 @@ const canDestroy = computed(
220
249
  class="flex items-center gap-2 rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-2 text-[12px] text-slate-300"
221
250
  >
222
251
  <UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-amber-300" />
223
- {{ phase ? PHASE_LABEL[phase] : '' }}
252
+ {{ phase ? t(PHASE_LABEL[phase]) : '' }}
224
253
  </p>
225
254
 
226
255
  <!-- Findings / fix -->
@@ -230,20 +259,20 @@ const canDestroy = computed(
230
259
  >
231
260
  <div class="flex items-center justify-between">
232
261
  <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
233
- Found a problem?
262
+ {{ t('humanTest.fix.heading') }}
234
263
  </h3>
235
264
  <button
236
265
  class="text-[12px] text-slate-400 hover:text-slate-200"
237
266
  @click="showFindings = !showFindings"
238
267
  >
239
- {{ showFindings ? 'Cancel' : 'Request a fix' }}
268
+ {{ showFindings ? t('humanTest.fix.cancel') : t('humanTest.fix.requestFix') }}
240
269
  </button>
241
270
  </div>
242
271
  <div v-if="showFindings" class="mt-2 space-y-2">
243
272
  <textarea
244
273
  v-model="findings"
245
274
  rows="4"
246
- placeholder="Describe what went wrong — the Fixer agent gets this as context, then the environment is rebuilt for re-testing."
275
+ :placeholder="t('humanTest.fix.placeholder')"
247
276
  class="w-full rounded-md border border-slate-700 bg-slate-950 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500 focus:outline-none"
248
277
  />
249
278
  <UButton
@@ -254,7 +283,7 @@ const canDestroy = computed(
254
283
  :disabled="busy || !findings.trim()"
255
284
  @click="submitFix"
256
285
  >
257
- Send to Fixer
286
+ {{ t('humanTest.fix.send') }}
258
287
  </UButton>
259
288
  </div>
260
289
  </section>
@@ -265,7 +294,7 @@ const canDestroy = computed(
265
294
  class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
266
295
  >
267
296
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
268
- History ({{ ht.attempts }} round{{ ht.attempts === 1 ? '' : 's' }})
297
+ {{ t('humanTest.history.heading', { count: ht.attempts }, ht.attempts) }}
269
298
  </h3>
270
299
  <ol class="space-y-2">
271
300
  <li v-for="(r, i) in ht.rounds" :key="i" class="flex items-start gap-2 text-[12px]">
@@ -275,7 +304,9 @@ const canDestroy = computed(
275
304
  />
276
305
  <div class="min-w-0 flex-1">
277
306
  <span class="text-slate-200">{{
278
- r.kind === 'fix' ? 'Fix requested' : 'Pulled main'
307
+ r.kind === 'fix'
308
+ ? t('humanTest.history.fixRequested')
309
+ : t('humanTest.history.pulledMain')
279
310
  }}</span>
280
311
  <span
281
312
  class="ml-1.5 rounded px-1 text-[10px] uppercase"
@@ -287,7 +318,11 @@ const canDestroy = computed(
287
318
  : 'bg-slate-500/15 text-slate-300'
288
319
  "
289
320
  >
290
- {{ r.outcome ?? 'in progress' }}
321
+ {{
322
+ r.outcome
323
+ ? t(ROUND_OUTCOME_LABEL[r.outcome])
324
+ : t('humanTest.roundOutcome.inProgress')
325
+ }}
291
326
  </span>
292
327
  <p v-if="r.findings" class="leading-snug text-slate-400">{{ r.findings }}</p>
293
328
  </div>
@@ -318,7 +353,7 @@ const canDestroy = computed(
318
353
  :disabled="busy || !awaitingHuman"
319
354
  @click="confirm"
320
355
  >
321
- Looks good — continue
356
+ {{ t('humanTest.confirm') }}
322
357
  </UButton>
323
358
  </footer>
324
359
  </div>
@@ -8,6 +8,7 @@ import StepMetadataCard from '~/components/panels/StepMetadataCard.vue'
8
8
  import StepTestReport from '~/components/panels/StepTestReport.vue'
9
9
  import EnvironmentStatusPanel from '~/components/environments/EnvironmentStatusPanel.vue'
10
10
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
11
+ import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
11
12
  import { useStepTimer } from '~/composables/useStepTimer'
12
13
  import { useStepProse } from '~/composables/useStepProse'
13
14
  import { useStepApproval } from '~/composables/useStepApproval'
@@ -12,6 +12,7 @@ import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue
12
12
  import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
13
13
  import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
14
14
  import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
15
+ import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
15
16
  import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
16
17
  import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
17
18
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'