@cat-factory/app 0.137.2 → 0.138.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.
@@ -8,7 +8,7 @@
8
8
  import { computed, ref } from 'vue'
9
9
  import { agentKindMeta } from '~/utils/catalog'
10
10
  import type { GateAttempt, GateStepState } from '~/types/execution'
11
- import StepRestartControl from '~/components/panels/StepRestartControl.vue'
11
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
12
12
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
13
13
  import AttemptEntryHeader from '~/components/panels/AttemptEntryHeader.vue'
14
14
  import GateFailingCheckList from '~/components/gates/GateFailingCheckList.vue'
@@ -20,10 +20,16 @@ const { t } = useI18n()
20
20
  const access = useWorkspaceAccess()
21
21
 
22
22
  // Synchronous window: it reads its state straight off the execution step, so there's
23
- // nothing to fetch on open (no `onOpen` loader).
24
- const { open, blockId, instanceId, stepIndex, close } = useResultView('gate')
23
+ // nothing to fetch on open (no `onOpen` loader). `manageEscape: false` — `ResultWindowShell`
24
+ // owns Escape (and focus trap + scroll lock + stacking).
25
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('gate', {
26
+ manageEscape: false,
27
+ })
25
28
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
26
29
  const prUrl = computed(() => block.value?.pullRequest?.url ?? null)
30
+ const headerTitle = computed(
31
+ () => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
32
+ )
27
33
 
28
34
  const instance = computed(() =>
29
35
  instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
@@ -171,363 +177,333 @@ const conflictVerdict = computed(() => {
171
177
  </script>
172
178
 
173
179
  <template>
174
- <Teleport to="body">
175
- <div
176
- v-if="open"
177
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
178
- @click.self="close"
179
- >
180
- <div
181
- class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
182
- role="dialog"
183
- aria-modal="true"
180
+ <ResultWindowShell
181
+ :open="open"
182
+ :icon="meta.icon"
183
+ icon-class="bg-sky-500/15 text-sky-300"
184
+ :title="headerTitle"
185
+ :subtitle="subtitle"
186
+ :step-ref="{ instanceId, stepIndex }"
187
+ width="3xl"
188
+ @close="close"
189
+ >
190
+ <template #header-extras>
191
+ <UBadge
192
+ :color="STATUS_META[status].badge"
193
+ variant="subtle"
194
+ size="sm"
195
+ data-testid="gate-status"
184
196
  >
185
- <!-- Header -->
186
- <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
187
- <span
188
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-sky-500/15 text-sky-300"
197
+ {{ STATUS_META[status].label }}
198
+ </UBadge>
199
+ </template>
200
+
201
+ <div class="flex min-h-0 flex-1">
202
+ <!-- Main: the conclusion -->
203
+ <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
204
+ <div
205
+ v-if="!gate"
206
+ class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
207
+ >
208
+ <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
209
+ <p class="text-sm">{{ t('gates.noActivity') }}</p>
210
+ <p class="max-w-sm text-[11px] text-slate-500">
211
+ {{ t('gates.noActivityHint') }}
212
+ </p>
213
+ </div>
214
+
215
+ <template v-else>
216
+ <!-- Passed -->
217
+ <div
218
+ v-if="status === 'passed'"
219
+ class="flex items-start gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-3 py-2.5"
189
220
  >
190
- <UIcon :name="meta.icon" class="h-4 w-4" />
191
- </span>
192
- <div class="min-w-0 flex-1">
193
- <h2 class="truncate text-sm font-semibold text-slate-100">
194
- {{ meta.label }}{{ block ? ` — ${block.title}` : '' }}
195
- </h2>
196
- <p class="truncate text-[11px] text-slate-400">{{ subtitle }}</p>
221
+ <UIcon name="i-lucide-circle-check" class="mt-0.5 h-4 w-4 shrink-0 text-emerald-400" />
222
+ <p class="text-[13px] leading-relaxed text-emerald-200">
223
+ {{ step?.output || (isCi ? t('gates.passedCi') : t('gates.passedConflicts')) }}
224
+ </p>
197
225
  </div>
198
- <UBadge :color="STATUS_META[status].badge" variant="subtle" size="sm">
199
- {{ STATUS_META[status].label }}
200
- </UBadge>
201
- <StepRestartControl
202
- :instance-id="instanceId"
203
- :step-index="stepIndex"
204
- @restarted="close"
205
- />
206
- <button
207
- class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
208
- @click="close"
209
- >
210
- <UIcon name="i-lucide-x" class="h-4 w-4" />
211
- </button>
212
- </header>
213
226
 
214
- <div class="flex min-h-0 flex-1">
215
- <!-- Main: the conclusion -->
216
- <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
227
+ <!-- Human review: approval progress, the feedback being fixed, freeform fix box -->
228
+ <template v-else-if="isHumanReview">
229
+ <div
230
+ class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
231
+ >
232
+ <UIcon name="i-lucide-users" class="h-4 w-4 shrink-0 text-violet-300" />
233
+ <span class="text-[13px] text-slate-200">
234
+ {{
235
+ t(
236
+ 'gates.humanReview.approvals',
237
+ { approved: gate.lastApprovals ?? 0, required: requiredApprovals },
238
+ requiredApprovals,
239
+ )
240
+ }}
241
+ <template v-if="status === 'fixing'">
242
+ {{ t('gates.humanReview.suffixFixing') }}</template
243
+ >
244
+ <template v-else-if="status === 'failing'">
245
+ {{ t('gates.humanReview.suffixFailing') }}</template
246
+ >
247
+ <template v-else> {{ t('gates.humanReview.suffixAwaiting') }}</template>
248
+ </span>
249
+ </div>
217
250
  <div
218
- v-if="!gate"
219
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
251
+ v-if="gate.lastFailureSummary"
252
+ class="relative mt-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
220
253
  >
221
- <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
222
- <p class="text-sm">{{ t('gates.noActivity') }}</p>
223
- <p class="max-w-sm text-[11px] text-slate-500">
224
- {{ t('gates.noActivityHint') }}
254
+ <CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
255
+ <p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
256
+ {{ gate.lastFailureSummary }}
225
257
  </p>
226
258
  </div>
259
+ <a
260
+ v-if="prUrl"
261
+ :href="prUrl"
262
+ target="_blank"
263
+ rel="noopener"
264
+ class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
265
+ >
266
+ {{ t('gates.humanReview.reviewPr') }}
267
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" />
268
+ </a>
227
269
 
228
- <template v-else>
229
- <!-- Passed -->
230
- <div
231
- v-if="status === 'passed'"
232
- class="flex items-start gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-3 py-2.5"
233
- >
234
- <UIcon
235
- name="i-lucide-circle-check"
236
- class="mt-0.5 h-4 w-4 shrink-0 text-emerald-400"
237
- />
238
- <p class="text-[13px] leading-relaxed text-emerald-200">
239
- {{ step?.output || (isCi ? t('gates.passedCi') : t('gates.passedConflicts')) }}
240
- </p>
270
+ <!-- Freeform fix request: dispatch the fixer now with these instructions. -->
271
+ <section v-if="status !== 'gave-up'" class="mt-4">
272
+ <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
273
+ {{ t('gates.humanReview.requestFixHeading') }}
274
+ </h3>
275
+ <p class="mb-2 text-[11px] leading-relaxed text-slate-500">
276
+ {{ t('gates.humanReview.requestFixDescription') }}
277
+ </p>
278
+ <textarea
279
+ v-model="fixInstructions"
280
+ rows="3"
281
+ :disabled="fixBusy"
282
+ :placeholder="t('gates.humanReview.requestFixPlaceholder')"
283
+ class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60"
284
+ />
285
+ <div class="mt-2 flex justify-end">
286
+ <UButton
287
+ size="sm"
288
+ color="primary"
289
+ icon="i-lucide-wrench"
290
+ :loading="fixBusy"
291
+ :disabled="
292
+ fixBusy || fixInstructions.trim().length === 0 || !access.canExecuteRuns.value
293
+ "
294
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
295
+ @click="submitFix"
296
+ >
297
+ {{ t('gates.humanReview.requestFix') }}
298
+ </UButton>
241
299
  </div>
300
+ </section>
301
+ </template>
242
302
 
243
- <!-- Human review: approval progress, the feedback being fixed, freeform fix box -->
244
- <template v-else-if="isHumanReview">
245
- <div
246
- class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
247
- >
248
- <UIcon name="i-lucide-users" class="h-4 w-4 shrink-0 text-violet-300" />
249
- <span class="text-[13px] text-slate-200">
250
- {{
251
- t(
252
- 'gates.humanReview.approvals',
253
- { approved: gate.lastApprovals ?? 0, required: requiredApprovals },
254
- requiredApprovals,
255
- )
256
- }}
257
- <template v-if="status === 'fixing'">
258
- {{ t('gates.humanReview.suffixFixing') }}</template
259
- >
260
- <template v-else-if="status === 'failing'">
261
- {{ t('gates.humanReview.suffixFailing') }}</template
262
- >
263
- <template v-else> {{ t('gates.humanReview.suffixAwaiting') }}</template>
264
- </span>
265
- </div>
266
- <div
267
- v-if="gate.lastFailureSummary"
268
- class="relative mt-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
269
- >
270
- <CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
271
- <p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
272
- {{ gate.lastFailureSummary }}
273
- </p>
274
- </div>
275
- <a
276
- v-if="prUrl"
277
- :href="prUrl"
278
- target="_blank"
279
- rel="noopener"
280
- class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
281
- >
282
- {{ t('gates.humanReview.reviewPr') }}
283
- <UIcon name="i-lucide-external-link" class="h-3 w-3" />
284
- </a>
303
+ <!-- CI: failing checks -->
304
+ <template v-else-if="isCi">
305
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
306
+ {{ t('gates.ci.failingChecks') }}
307
+ </h3>
308
+ <GateFailingCheckList v-if="failingChecks.length" :checks="failingChecks" />
309
+ <p v-else class="text-[13px] leading-relaxed text-slate-300">
310
+ {{ gate.lastFailureSummary || t('gates.ci.failureFallback') }}
311
+ </p>
312
+ </template>
285
313
 
286
- <!-- Freeform fix request: dispatch the fixer now with these instructions. -->
287
- <section v-if="status !== 'gave-up'" class="mt-4">
288
- <h3
289
- class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
290
- >
291
- {{ t('gates.humanReview.requestFixHeading') }}
292
- </h3>
293
- <p class="mb-2 text-[11px] leading-relaxed text-slate-500">
294
- {{ t('gates.humanReview.requestFixDescription') }}
295
- </p>
296
- <textarea
297
- v-model="fixInstructions"
298
- rows="3"
299
- :disabled="fixBusy"
300
- :placeholder="t('gates.humanReview.requestFixPlaceholder')"
301
- class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60"
302
- />
303
- <div class="mt-2 flex justify-end">
304
- <UButton
305
- size="sm"
306
- color="primary"
307
- icon="i-lucide-wrench"
308
- :loading="fixBusy"
309
- :disabled="
310
- fixBusy ||
311
- fixInstructions.trim().length === 0 ||
312
- !access.canExecuteRuns.value
313
- "
314
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
315
- @click="submitFix"
316
- >
317
- {{ t('gates.humanReview.requestFix') }}
318
- </UButton>
319
- </div>
320
- </section>
321
- </template>
314
+ <!-- Doc quality: the deterministic structural findings the gate raised -->
315
+ <template v-else-if="isDocQuality">
316
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
317
+ {{ t('gates.docQuality.findings') }}
318
+ </h3>
319
+ <div
320
+ v-if="gate.lastFailureSummary"
321
+ class="relative rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
322
+ >
323
+ <CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
324
+ <p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
325
+ {{ gate.lastFailureSummary }}
326
+ </p>
327
+ </div>
328
+ <p v-else class="text-[13px] leading-relaxed text-slate-300">
329
+ {{ t('gates.docQuality.findingsFallback') }}
330
+ </p>
331
+ <a
332
+ v-if="prUrl"
333
+ :href="prUrl"
334
+ target="_blank"
335
+ rel="noopener"
336
+ class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
337
+ >
338
+ {{ t('gates.docQuality.viewPr') }}
339
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" />
340
+ </a>
341
+ </template>
322
342
 
323
- <!-- CI: failing checks -->
324
- <template v-else-if="isCi">
325
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
326
- {{ t('gates.ci.failingChecks') }}
327
- </h3>
328
- <GateFailingCheckList v-if="failingChecks.length" :checks="failingChecks" />
329
- <p v-else class="text-[13px] leading-relaxed text-slate-300">
330
- {{ gate.lastFailureSummary || t('gates.ci.failureFallback') }}
331
- </p>
332
- </template>
343
+ <!-- Conflicts: verdict + the resolver's account of what it left -->
344
+ <template v-else>
345
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
346
+ {{ t('gates.conflicts.mergeability') }}
347
+ </h3>
348
+ <div
349
+ class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
350
+ >
351
+ <UIcon
352
+ :name="STATUS_META[status].icon"
353
+ class="h-4 w-4 shrink-0"
354
+ :class="STATUS_META[status].text"
355
+ />
356
+ <span class="text-[13px] text-slate-200">{{ conflictVerdict }}</span>
357
+ </div>
358
+ <!-- GitHub's API reports mergeability as a single bit (no file list), but the
359
+ conflict resolver discovers the conflicting files in the container and
360
+ reports them back — surface that account here. -->
361
+ <div
362
+ v-if="gate.lastFailureSummary"
363
+ class="relative mt-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
364
+ >
365
+ <CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
366
+ <p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
367
+ {{ gate.lastFailureSummary }}
368
+ </p>
369
+ </div>
370
+ <a
371
+ v-if="prUrl"
372
+ :href="prUrl"
373
+ target="_blank"
374
+ rel="noopener"
375
+ class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
376
+ >
377
+ {{ t('gates.conflicts.viewPr') }}
378
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" />
379
+ </a>
380
+ </template>
333
381
 
334
- <!-- Doc quality: the deterministic structural findings the gate raised -->
335
- <template v-else-if="isDocQuality">
336
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
337
- {{ t('gates.docQuality.findings') }}
338
- </h3>
382
+ <!-- Attempt history (both gates): what each helper run did and how it ended. -->
383
+ <section v-if="attempts.length" class="mt-5">
384
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
385
+ {{ t('gates.attemptsHeading', { helper: helperMeta.label }) }}
386
+ </h3>
387
+ <ol class="space-y-2">
388
+ <li
389
+ v-for="a in attempts"
390
+ :key="a.attempt"
391
+ class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
392
+ >
393
+ <AttemptEntryHeader
394
+ :label="t('gates.attempt', { number: a.attempt })"
395
+ :outcome="a.outcome"
396
+ :outcome-label="OUTCOME_LABELS[a.outcome]"
397
+ :at="a.at"
398
+ date-format="long"
399
+ />
400
+ <!-- What this round was asked to fix: the instructions the gate handed the
401
+ helper (the failing-check summary / conflict reason / review comments),
402
+ plus the structured red checks for the CI gate. -->
339
403
  <div
340
- v-if="gate.lastFailureSummary"
341
- class="relative rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
404
+ v-if="a.instructions || (a.failingChecks && a.failingChecks.length)"
405
+ class="mt-1.5"
342
406
  >
343
- <CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
344
- <p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
345
- {{ gate.lastFailureSummary }}
407
+ <p class="text-[11px] text-slate-500">
408
+ {{ t('gates.attemptInstructions', { helper: helperMeta.label }) }}
346
409
  </p>
347
- </div>
348
- <p v-else class="text-[13px] leading-relaxed text-slate-300">
349
- {{ t('gates.docQuality.findingsFallback') }}
350
- </p>
351
- <a
352
- v-if="prUrl"
353
- :href="prUrl"
354
- target="_blank"
355
- rel="noopener"
356
- class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
357
- >
358
- {{ t('gates.docQuality.viewPr') }}
359
- <UIcon name="i-lucide-external-link" class="h-3 w-3" />
360
- </a>
361
- </template>
362
-
363
- <!-- Conflicts: verdict + the resolver's account of what it left -->
364
- <template v-else>
365
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
366
- {{ t('gates.conflicts.mergeability') }}
367
- </h3>
368
- <div
369
- class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
370
- >
371
- <UIcon
372
- :name="STATUS_META[status].icon"
373
- class="h-4 w-4 shrink-0"
374
- :class="STATUS_META[status].text"
410
+ <GateFailingCheckList
411
+ v-if="a.failingChecks && a.failingChecks.length"
412
+ class="mt-1"
413
+ :checks="a.failingChecks"
414
+ dense
375
415
  />
376
- <span class="text-[13px] text-slate-200">{{ conflictVerdict }}</span>
377
- </div>
378
- <!-- GitHub's API reports mergeability as a single bit (no file list), but the
379
- conflict resolver discovers the conflicting files in the container and
380
- reports them back — surface that account here. -->
381
- <div
382
- v-if="gate.lastFailureSummary"
383
- class="relative mt-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
384
- >
385
- <CopyButton :text="gate.lastFailureSummary" class="absolute end-1 top-1" />
386
- <p class="whitespace-pre-wrap pe-8 text-[12px] leading-relaxed text-slate-300">
387
- {{ gate.lastFailureSummary }}
416
+ <p
417
+ v-else-if="a.instructions"
418
+ class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-300"
419
+ >
420
+ {{ a.instructions }}
388
421
  </p>
389
422
  </div>
390
- <a
391
- v-if="prUrl"
392
- :href="prUrl"
393
- target="_blank"
394
- rel="noopener"
395
- class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
396
- >
397
- {{ t('gates.conflicts.viewPr') }}
398
- <UIcon name="i-lucide-external-link" class="h-3 w-3" />
399
- </a>
400
- </template>
423
+ <!-- The helper's own report of what it did / what remains. -->
424
+ <template v-if="a.summary">
425
+ <p class="mt-1.5 text-[11px] text-slate-500">
426
+ {{ t('gates.attemptReport', { helper: helperMeta.label }) }}
427
+ </p>
428
+ <p class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400">
429
+ {{ a.summary }}
430
+ </p>
431
+ </template>
432
+ </li>
433
+ </ol>
434
+ </section>
435
+ </template>
436
+ </div>
401
437
 
402
- <!-- Attempt history (both gates): what each helper run did and how it ended. -->
403
- <section v-if="attempts.length" class="mt-5">
404
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
405
- {{ t('gates.attemptsHeading', { helper: helperMeta.label }) }}
406
- </h3>
407
- <ol class="space-y-2">
408
- <li
409
- v-for="a in attempts"
410
- :key="a.attempt"
411
- class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
412
- >
413
- <AttemptEntryHeader
414
- :label="t('gates.attempt', { number: a.attempt })"
415
- :outcome="a.outcome"
416
- :outcome-label="OUTCOME_LABELS[a.outcome]"
417
- :at="a.at"
418
- date-format="long"
419
- />
420
- <!-- What this round was asked to fix: the instructions the gate handed the
421
- helper (the failing-check summary / conflict reason / review comments),
422
- plus the structured red checks for the CI gate. -->
423
- <div
424
- v-if="a.instructions || (a.failingChecks && a.failingChecks.length)"
425
- class="mt-1.5"
426
- >
427
- <p class="text-[11px] text-slate-500">
428
- {{ t('gates.attemptInstructions', { helper: helperMeta.label }) }}
429
- </p>
430
- <GateFailingCheckList
431
- v-if="a.failingChecks && a.failingChecks.length"
432
- class="mt-1"
433
- :checks="a.failingChecks"
434
- dense
435
- />
436
- <p
437
- v-else-if="a.instructions"
438
- class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-300"
439
- >
440
- {{ a.instructions }}
441
- </p>
442
- </div>
443
- <!-- The helper's own report of what it did / what remains. -->
444
- <template v-if="a.summary">
445
- <p class="mt-1.5 text-[11px] text-slate-500">
446
- {{ t('gates.attemptReport', { helper: helperMeta.label }) }}
447
- </p>
448
- <p
449
- class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400"
450
- >
451
- {{ a.summary }}
452
- </p>
453
- </template>
454
- </li>
455
- </ol>
456
- </section>
457
- </template>
438
+ <!-- Sidebar: gate state -->
439
+ <aside
440
+ class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
441
+ >
442
+ <div v-if="gate">
443
+ <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
444
+ {{ t('gates.sidebar.state') }}
445
+ </h4>
446
+ <div class="flex items-center gap-2 text-[13px]">
447
+ <UIcon
448
+ :name="STATUS_META[status].icon"
449
+ class="h-4 w-4"
450
+ :class="STATUS_META[status].text"
451
+ />
452
+ <span :class="STATUS_META[status].text">{{ STATUS_META[status].label }}</span>
458
453
  </div>
454
+ </div>
459
455
 
460
- <!-- Sidebar: gate state -->
461
- <aside
462
- class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
463
- >
464
- <div v-if="gate">
465
- <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
466
- {{ t('gates.sidebar.state') }}
467
- </h4>
468
- <div class="flex items-center gap-2 text-[13px]">
469
- <UIcon
470
- :name="STATUS_META[status].icon"
471
- class="h-4 w-4"
472
- :class="STATUS_META[status].text"
473
- />
474
- <span :class="STATUS_META[status].text">{{ STATUS_META[status].label }}</span>
475
- </div>
476
- </div>
477
-
478
- <div v-if="gate">
479
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
480
- {{ helperMeta.label }}
481
- </h4>
482
- <p class="text-[12px] text-slate-300">
483
- <!-- The human-review gate's budget is effectively unbounded (it waits for a human
456
+ <div v-if="gate">
457
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
458
+ {{ helperMeta.label }}
459
+ </h4>
460
+ <p class="text-[12px] text-slate-300">
461
+ <!-- The human-review gate's budget is effectively unbounded (it waits for a human
484
462
  indefinitely), so render a plain round count rather than "0/9007199254740991". -->
485
- <template v-if="isHumanReview">
486
- {{ t('gates.sidebar.fixRounds', { count: gate.attempts }, gate.attempts) }}
487
- </template>
488
- <template v-else>
489
- {{
490
- t(
491
- 'gates.sidebar.attempts',
492
- { attempts: gate.attempts, max: gate.maxAttempts },
493
- gate.maxAttempts,
494
- )
495
- }}
496
- </template>
497
- <template v-if="gate.phase === 'working'">
498
- {{ t('gates.sidebar.suffixRunning') }}</template
499
- >
500
- <template v-else-if="gate.attempts === 0">
501
- {{ t('gates.sidebar.suffixNotNeeded') }}</template
502
- >
503
- </p>
504
- </div>
463
+ <template v-if="isHumanReview">
464
+ {{ t('gates.sidebar.fixRounds', { count: gate.attempts }, gate.attempts) }}
465
+ </template>
466
+ <template v-else>
467
+ {{
468
+ t(
469
+ 'gates.sidebar.attempts',
470
+ { attempts: gate.attempts, max: gate.maxAttempts },
471
+ gate.maxAttempts,
472
+ )
473
+ }}
474
+ </template>
475
+ <template v-if="gate.phase === 'working'">
476
+ {{ t('gates.sidebar.suffixRunning') }}</template
477
+ >
478
+ <template v-else-if="gate.attempts === 0">
479
+ {{ t('gates.sidebar.suffixNotNeeded') }}</template
480
+ >
481
+ </p>
482
+ </div>
505
483
 
506
- <div v-if="shortSha">
507
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
508
- {{ t('gates.sidebar.gatedCommit') }}
509
- </h4>
510
- <p class="font-mono text-[12px] text-slate-300">{{ shortSha }}</p>
511
- </div>
484
+ <div v-if="shortSha">
485
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
486
+ {{ t('gates.sidebar.gatedCommit') }}
487
+ </h4>
488
+ <p class="font-mono text-[12px] text-slate-300">{{ shortSha }}</p>
489
+ </div>
512
490
 
513
- <!-- Shared run metadata + embedded observability (model, run id, timing,
491
+ <!-- Shared run metadata + embedded observability (model, run id, timing,
514
492
  model-activity rollup) — identical to the agent step detail. -->
515
- <StepRunMeta
516
- v-if="step"
517
- :step="step"
518
- :instance-id="instanceId ?? undefined"
519
- :step-number="stepIndex === null ? undefined : stepIndex + 1"
520
- :total-steps="instance?.steps.length"
521
- :run-failed="instance?.status === 'failed'"
522
- :failure-at="instance?.failure?.occurredAt"
523
- />
493
+ <StepRunMeta
494
+ v-if="step"
495
+ :step="step"
496
+ :instance-id="instanceId ?? undefined"
497
+ :step-number="stepIndex === null ? undefined : stepIndex + 1"
498
+ :total-steps="instance?.steps.length"
499
+ :run-failed="instance?.status === 'failed'"
500
+ :failure-at="instance?.failure?.occurredAt"
501
+ />
524
502
 
525
- <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
526
- {{ t('gates.sidebar.footer', { helper: helperMeta.label }) }}
527
- </p>
528
- </aside>
529
- </div>
530
- </div>
503
+ <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
504
+ {{ t('gates.sidebar.footer', { helper: helperMeta.label }) }}
505
+ </p>
506
+ </aside>
531
507
  </div>
532
- </Teleport>
508
+ </ResultWindowShell>
533
509
  </template>