@cat-factory/app 0.137.1 → 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.
- package/app/components/followUp/FollowUpWindow.vue +174 -200
- package/app/components/forkDecision/ForkDecisionWindow.vue +250 -275
- package/app/components/gates/GateResultView.vue +309 -333
- package/app/components/github/AddServiceFromRepoModal.vue +4 -0
- package/app/components/humanTest/HumanTestWindow.vue +212 -236
- package/app/components/media/ArtifactLightbox.vue +20 -19
- package/app/components/panels/GenericStructuredResultView.vue +60 -88
- package/app/components/ralph/RalphLoopResultView.vue +150 -171
- package/app/components/testing/TestReportWindow.vue +505 -559
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +224 -262
- package/app/modular/panels/inspector.logic.spec.ts +18 -0
- package/app/modular/panels/inspector.logic.ts +14 -4
- package/i18n/locales/de.json +0 -1
- package/i18n/locales/en.json +0 -1
- package/i18n/locales/es.json +0 -1
- package/i18n/locales/fr.json +0 -1
- package/i18n/locales/he.json +0 -1
- package/i18n/locales/it.json +0 -1
- package/i18n/locales/ja.json +0 -1
- package/i18n/locales/pl.json +0 -1
- package/i18n/locales/tr.json +0 -1
- package/i18n/locales/uk.json +0 -1
- package/package.json +1 -1
- package/app/composables/useFocusTrap.ts +0 -72
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
import { computed, onUnmounted, reactive, ref, watch } from 'vue'
|
|
11
11
|
import type { VisualConfirmStepState } from '~/types/execution'
|
|
12
12
|
import { useArtifactBlobs } from '~/composables/useArtifactBlobs'
|
|
13
|
-
import { useFocusTrap } from '~/composables/useFocusTrap'
|
|
14
13
|
import ImageCompare from '~/components/media/ImageCompare.vue'
|
|
15
14
|
import ArtifactLightbox from '~/components/media/ArtifactLightbox.vue'
|
|
15
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
16
16
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
17
17
|
|
|
18
18
|
const board = useBoardStore()
|
|
@@ -26,8 +26,17 @@ const access = useWorkspaceAccess()
|
|
|
26
26
|
const blobs = useArtifactBlobs()
|
|
27
27
|
onUnmounted(() => blobs.revokeAll())
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// `manageEscape: false` — `ResultWindowShell` owns Escape (and the focus trap + scroll lock +
|
|
30
|
+
// stacking); the nested lightbox layers above it on the same shared overlay stack.
|
|
31
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('visual-confirm', {
|
|
32
|
+
manageEscape: false,
|
|
33
|
+
})
|
|
30
34
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
35
|
+
const headerTitle = computed(() =>
|
|
36
|
+
block.value
|
|
37
|
+
? t('visualConfirm.titleWithBlock', { title: block.value.title })
|
|
38
|
+
: t('visualConfirm.title'),
|
|
39
|
+
)
|
|
31
40
|
|
|
32
41
|
const instance = computed(() =>
|
|
33
42
|
instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
|
|
@@ -95,14 +104,6 @@ function expand(artifactId: string) {
|
|
|
95
104
|
lightboxOpen.value = true
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
// Focus management for the modal panel. While the lightbox is open it owns the trap, so the
|
|
99
|
-
// window hands off (active = open && !lightbox) to avoid two Tab traps fighting.
|
|
100
|
-
const dialogRoot = ref<HTMLElement | null>(null)
|
|
101
|
-
useFocusTrap(
|
|
102
|
-
dialogRoot,
|
|
103
|
-
computed(() => open.value && !lightboxOpen.value),
|
|
104
|
-
)
|
|
105
|
-
|
|
106
107
|
// --- Request a fix: per-view notes + a freeform box, composed into one findings string. ---
|
|
107
108
|
const perViewNotes = reactive<Record<string, string>>({})
|
|
108
109
|
const noteOpen = reactive<Record<string, boolean>>({})
|
|
@@ -187,278 +188,239 @@ const canApprove = computed(
|
|
|
187
188
|
</script>
|
|
188
189
|
|
|
189
190
|
<template>
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
<ResultWindowShell
|
|
192
|
+
:open="open"
|
|
193
|
+
icon="i-lucide-image-play"
|
|
194
|
+
icon-class="bg-amber-500/15 text-amber-300"
|
|
195
|
+
:title="headerTitle"
|
|
196
|
+
:subtitle="phase ? PHASE_LABEL[phase] : t('visualConfirm.subtitle')"
|
|
197
|
+
width="5xl"
|
|
198
|
+
@close="close"
|
|
199
|
+
>
|
|
200
|
+
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4">
|
|
196
201
|
<div
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
role="dialog"
|
|
200
|
-
aria-modal="true"
|
|
201
|
-
:aria-label="t('visualConfirm.ariaLabel')"
|
|
202
|
-
class="m-4 flex w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl focus:outline-none"
|
|
202
|
+
v-if="!vc"
|
|
203
|
+
class="flex flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
203
204
|
>
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
>
|
|
208
|
-
<UIcon name="i-lucide-image-play" class="h-4 w-4" />
|
|
209
|
-
</span>
|
|
210
|
-
<div class="min-w-0 flex-1">
|
|
211
|
-
<h2 class="truncate text-sm font-semibold text-slate-100">
|
|
212
|
-
{{
|
|
213
|
-
block
|
|
214
|
-
? t('visualConfirm.titleWithBlock', { title: block.title })
|
|
215
|
-
: t('visualConfirm.title')
|
|
216
|
-
}}
|
|
217
|
-
</h2>
|
|
218
|
-
<p class="truncate text-[11px] text-slate-400">
|
|
219
|
-
{{ phase ? PHASE_LABEL[phase] : t('visualConfirm.subtitle') }}
|
|
220
|
-
</p>
|
|
221
|
-
</div>
|
|
222
|
-
<button
|
|
223
|
-
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
224
|
-
@click="close"
|
|
225
|
-
>
|
|
226
|
-
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
227
|
-
</button>
|
|
228
|
-
</header>
|
|
229
|
-
|
|
230
|
-
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4">
|
|
231
|
-
<div
|
|
232
|
-
v-if="!vc"
|
|
233
|
-
class="flex flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
234
|
-
>
|
|
235
|
-
<UIcon name="i-lucide-image-play" class="h-8 w-8 opacity-40" />
|
|
236
|
-
<p class="text-sm">{{ t('visualConfirm.notStarted') }}</p>
|
|
237
|
-
</div>
|
|
205
|
+
<UIcon name="i-lucide-image-play" class="h-8 w-8 opacity-40" />
|
|
206
|
+
<p class="text-sm">{{ t('visualConfirm.notStarted') }}</p>
|
|
207
|
+
</div>
|
|
238
208
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
209
|
+
<template v-else>
|
|
210
|
+
<p
|
|
211
|
+
v-if="vc.degradedReason"
|
|
212
|
+
class="rounded-lg border border-amber-700/40 bg-amber-500/5 px-3 py-2 text-[12px] text-amber-300/90"
|
|
213
|
+
>
|
|
214
|
+
{{ vc.degradedReason }}
|
|
215
|
+
</p>
|
|
246
216
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
217
|
+
<p
|
|
218
|
+
v-if="working"
|
|
219
|
+
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"
|
|
220
|
+
>
|
|
221
|
+
<UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-amber-300" />
|
|
222
|
+
{{ phase ? PHASE_LABEL[phase] : '' }}
|
|
223
|
+
</p>
|
|
254
224
|
|
|
255
|
-
|
|
225
|
+
<!-- Actual-vs-reference gallery. Keyed by `view` (the contract's unique per-pair
|
|
256
226
|
identity) so a pair's note/expand state stays bound to its view across recaptures. -->
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
/>
|
|
278
|
-
{{ t('visualConfirm.noteIssue', { view: p.view }) }}
|
|
279
|
-
<span
|
|
280
|
-
v-if="perViewNotes[p.view]?.trim()"
|
|
281
|
-
class="rounded-full bg-amber-500/15 px-1.5 text-[9px] text-amber-300"
|
|
282
|
-
>{{ t('visualConfirm.noted') }}</span
|
|
283
|
-
>
|
|
284
|
-
</button>
|
|
285
|
-
<textarea
|
|
286
|
-
v-if="noteOpen[p.view]"
|
|
287
|
-
v-model="perViewNotes[p.view]"
|
|
288
|
-
rows="2"
|
|
289
|
-
:placeholder="t('visualConfirm.notePlaceholder', { view: p.view })"
|
|
290
|
-
class="mt-1 w-full rounded-md border border-slate-700 bg-slate-950 px-2 py-1.5 text-[12px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60"
|
|
291
|
-
/>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
</section>
|
|
295
|
-
<p v-else class="text-[12px] italic text-slate-500">
|
|
296
|
-
{{ t('visualConfirm.noScreenshots') }}
|
|
297
|
-
</p>
|
|
298
|
-
|
|
299
|
-
<!-- Upload a reference for any view -->
|
|
300
|
-
<section class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
|
301
|
-
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
302
|
-
{{ t('visualConfirm.upload.heading') }}
|
|
303
|
-
</h3>
|
|
304
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
305
|
-
<input
|
|
306
|
-
v-model="uploadView"
|
|
307
|
-
list="vc-views"
|
|
308
|
-
:placeholder="t('visualConfirm.upload.viewPlaceholder')"
|
|
309
|
-
class="rounded-md border border-slate-700 bg-slate-950 px-2 py-1 text-[12px] text-slate-200 placeholder:text-slate-600"
|
|
310
|
-
/>
|
|
311
|
-
<datalist id="vc-views">
|
|
312
|
-
<option v-for="p in pairs" :key="p.view" :value="p.view" />
|
|
313
|
-
</datalist>
|
|
314
|
-
<input
|
|
315
|
-
ref="fileInput"
|
|
316
|
-
type="file"
|
|
317
|
-
accept="image/png,image/jpeg"
|
|
318
|
-
:disabled="busy || !uploadView.trim()"
|
|
319
|
-
class="text-[12px] text-slate-300 file:me-2 file:rounded file:border-0 file:bg-slate-800 file:px-2 file:py-1 file:text-slate-200 disabled:opacity-40"
|
|
320
|
-
@change="onFilePicked"
|
|
227
|
+
<section v-if="pairs.length" class="space-y-4">
|
|
228
|
+
<div v-for="p in pairs" :key="p.view" class="space-y-2">
|
|
229
|
+
<ImageCompare
|
|
230
|
+
:view="p.view"
|
|
231
|
+
:actual-id="p.actualArtifactId"
|
|
232
|
+
:reference-id="p.referenceArtifactId"
|
|
233
|
+
:blobs="blobs"
|
|
234
|
+
:busy="busy"
|
|
235
|
+
@expand="expand"
|
|
236
|
+
@upload-reference="(file: File) => uploadFor(p.view, file)"
|
|
237
|
+
/>
|
|
238
|
+
<!-- Per-view note (folded into the fixer findings) -->
|
|
239
|
+
<div v-if="awaitingHuman" class="px-1">
|
|
240
|
+
<button
|
|
241
|
+
class="flex items-center gap-1.5 text-[11px] text-slate-400 hover:text-slate-200"
|
|
242
|
+
@click="noteOpen[p.view] = !noteOpen[p.view]"
|
|
243
|
+
>
|
|
244
|
+
<UIcon
|
|
245
|
+
:name="noteOpen[p.view] ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
246
|
+
class="h-3 w-3"
|
|
321
247
|
/>
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
</p>
|
|
330
|
-
</section>
|
|
331
|
-
|
|
332
|
-
<!-- Request fix -->
|
|
333
|
-
<section
|
|
334
|
-
v-if="awaitingHuman"
|
|
335
|
-
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
336
|
-
>
|
|
337
|
-
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
338
|
-
{{ t('visualConfirm.requestFix.heading') }}
|
|
339
|
-
</h3>
|
|
248
|
+
{{ t('visualConfirm.noteIssue', { view: p.view }) }}
|
|
249
|
+
<span
|
|
250
|
+
v-if="perViewNotes[p.view]?.trim()"
|
|
251
|
+
class="rounded-full bg-amber-500/15 px-1.5 text-[9px] text-amber-300"
|
|
252
|
+
>{{ t('visualConfirm.noted') }}</span
|
|
253
|
+
>
|
|
254
|
+
</button>
|
|
340
255
|
<textarea
|
|
341
|
-
v-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
256
|
+
v-if="noteOpen[p.view]"
|
|
257
|
+
v-model="perViewNotes[p.view]"
|
|
258
|
+
rows="2"
|
|
259
|
+
:placeholder="t('visualConfirm.notePlaceholder', { view: p.view })"
|
|
260
|
+
class="mt-1 w-full rounded-md border border-slate-700 bg-slate-950 px-2 py-1.5 text-[12px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60"
|
|
345
261
|
/>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
color="warning"
|
|
353
|
-
icon="i-lucide-wrench"
|
|
354
|
-
:loading="busy"
|
|
355
|
-
:disabled="busy || !hasFindings || !access.canExecuteRuns.value"
|
|
356
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
357
|
-
@click="submitFix"
|
|
358
|
-
>
|
|
359
|
-
{{ t('visualConfirm.requestFix.send') }}
|
|
360
|
-
</UButton>
|
|
361
|
-
</div>
|
|
362
|
-
</section>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</section>
|
|
265
|
+
<p v-else class="text-[12px] italic text-slate-500">
|
|
266
|
+
{{ t('visualConfirm.noScreenshots') }}
|
|
267
|
+
</p>
|
|
363
268
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}}
|
|
397
|
-
</span>
|
|
398
|
-
<p v-if="r.findings" class="whitespace-pre-wrap leading-snug text-slate-400">
|
|
399
|
-
{{ r.findings }}
|
|
400
|
-
</p>
|
|
401
|
-
</div>
|
|
402
|
-
</li>
|
|
403
|
-
</ol>
|
|
404
|
-
</section>
|
|
405
|
-
</template>
|
|
406
|
-
</div>
|
|
269
|
+
<!-- Upload a reference for any view -->
|
|
270
|
+
<section class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
|
|
271
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
272
|
+
{{ t('visualConfirm.upload.heading') }}
|
|
273
|
+
</h3>
|
|
274
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
275
|
+
<input
|
|
276
|
+
v-model="uploadView"
|
|
277
|
+
list="vc-views"
|
|
278
|
+
:placeholder="t('visualConfirm.upload.viewPlaceholder')"
|
|
279
|
+
class="rounded-md border border-slate-700 bg-slate-950 px-2 py-1 text-[12px] text-slate-200 placeholder:text-slate-600"
|
|
280
|
+
/>
|
|
281
|
+
<datalist id="vc-views">
|
|
282
|
+
<option v-for="p in pairs" :key="p.view" :value="p.view" />
|
|
283
|
+
</datalist>
|
|
284
|
+
<input
|
|
285
|
+
ref="fileInput"
|
|
286
|
+
type="file"
|
|
287
|
+
accept="image/png,image/jpeg"
|
|
288
|
+
:disabled="busy || !uploadView.trim()"
|
|
289
|
+
class="text-[12px] text-slate-300 file:me-2 file:rounded file:border-0 file:bg-slate-800 file:px-2 file:py-1 file:text-slate-200 disabled:opacity-40"
|
|
290
|
+
@change="onFilePicked"
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
<p class="mt-1.5 text-[10px] text-slate-600">
|
|
294
|
+
{{
|
|
295
|
+
uploadView.trim()
|
|
296
|
+
? t('visualConfirm.upload.tipReady')
|
|
297
|
+
: t('visualConfirm.upload.tipNeedView')
|
|
298
|
+
}}
|
|
299
|
+
</p>
|
|
300
|
+
</section>
|
|
407
301
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
302
|
+
<!-- Request fix -->
|
|
303
|
+
<section
|
|
304
|
+
v-if="awaitingHuman"
|
|
305
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
411
306
|
>
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
:
|
|
419
|
-
:
|
|
307
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
308
|
+
{{ t('visualConfirm.requestFix.heading') }}
|
|
309
|
+
</h3>
|
|
310
|
+
<textarea
|
|
311
|
+
v-model="globalFindings"
|
|
312
|
+
rows="3"
|
|
313
|
+
:placeholder="t('visualConfirm.requestFix.placeholder')"
|
|
314
|
+
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 focus-visible:ring-2 focus-visible:ring-amber-500/60"
|
|
420
315
|
/>
|
|
421
|
-
<div class="flex items-center
|
|
422
|
-
<
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
>
|
|
426
|
-
<input v-model="ackDegraded" type="checkbox" class="accent-amber-500" />
|
|
427
|
-
{{ t('visualConfirm.reviewedManually') }}
|
|
428
|
-
</label>
|
|
316
|
+
<div class="mt-2 flex items-center justify-between">
|
|
317
|
+
<span class="text-[11px] text-slate-500">
|
|
318
|
+
{{ t('visualConfirm.requestFix.foldedHint') }}
|
|
319
|
+
</span>
|
|
429
320
|
<UButton
|
|
430
321
|
size="sm"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
icon="i-lucide-refresh-cw"
|
|
322
|
+
color="warning"
|
|
323
|
+
icon="i-lucide-wrench"
|
|
434
324
|
:loading="busy"
|
|
435
|
-
:disabled="busy || !
|
|
325
|
+
:disabled="busy || !hasFindings || !access.canExecuteRuns.value"
|
|
436
326
|
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
437
|
-
@click="
|
|
327
|
+
@click="submitFix"
|
|
438
328
|
>
|
|
439
|
-
{{ t('visualConfirm.
|
|
440
|
-
</UButton>
|
|
441
|
-
<UButton
|
|
442
|
-
color="primary"
|
|
443
|
-
icon="i-lucide-circle-check"
|
|
444
|
-
:loading="busy"
|
|
445
|
-
:disabled="!canApprove || !access.canExecuteRuns.value"
|
|
446
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
447
|
-
@click="approve"
|
|
448
|
-
>
|
|
449
|
-
{{ t('visualConfirm.approve') }}
|
|
329
|
+
{{ t('visualConfirm.requestFix.send') }}
|
|
450
330
|
</UButton>
|
|
451
331
|
</div>
|
|
452
|
-
</
|
|
453
|
-
|
|
332
|
+
</section>
|
|
333
|
+
|
|
334
|
+
<!-- Rounds history -->
|
|
335
|
+
<section
|
|
336
|
+
v-if="vc.rounds && vc.rounds.length"
|
|
337
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
338
|
+
>
|
|
339
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
340
|
+
{{ t('visualConfirm.history.heading', { count: vc.attempts }, vc.attempts) }}
|
|
341
|
+
</h3>
|
|
342
|
+
<ol class="space-y-2">
|
|
343
|
+
<li v-for="(r, i) in vc.rounds" :key="i" class="flex items-start gap-2 text-[12px]">
|
|
344
|
+
<UIcon name="i-lucide-wrench" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-slate-400" />
|
|
345
|
+
<div class="min-w-0 flex-1">
|
|
346
|
+
<span class="text-slate-200">{{ t('visualConfirm.history.fixRequested') }}</span>
|
|
347
|
+
<span
|
|
348
|
+
class="ms-1.5 rounded px-1 text-[10px] uppercase"
|
|
349
|
+
:class="
|
|
350
|
+
r.outcome === 'completed'
|
|
351
|
+
? 'bg-emerald-500/15 text-emerald-300'
|
|
352
|
+
: r.outcome === 'failed'
|
|
353
|
+
? 'bg-rose-500/15 text-rose-300'
|
|
354
|
+
: 'bg-slate-500/15 text-slate-300'
|
|
355
|
+
"
|
|
356
|
+
>
|
|
357
|
+
{{
|
|
358
|
+
r.outcome ? OUTCOME_LABELS[r.outcome] : t('visualConfirm.outcome.inProgress')
|
|
359
|
+
}}
|
|
360
|
+
</span>
|
|
361
|
+
<p v-if="r.findings" class="whitespace-pre-wrap leading-snug text-slate-400">
|
|
362
|
+
{{ r.findings }}
|
|
363
|
+
</p>
|
|
364
|
+
</div>
|
|
365
|
+
</li>
|
|
366
|
+
</ol>
|
|
367
|
+
</section>
|
|
368
|
+
</template>
|
|
454
369
|
</div>
|
|
455
370
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
371
|
+
<footer
|
|
372
|
+
v-if="vc"
|
|
373
|
+
class="flex items-center justify-between gap-3 border-t border-slate-800 px-5 py-3"
|
|
374
|
+
>
|
|
375
|
+
<StepRunMeta
|
|
376
|
+
v-if="step"
|
|
377
|
+
:step="step"
|
|
378
|
+
:instance-id="instanceId ?? undefined"
|
|
379
|
+
:step-number="stepIndex === null ? undefined : stepIndex + 1"
|
|
380
|
+
:total-steps="instance?.steps.length"
|
|
381
|
+
:run-failed="instance?.status === 'failed'"
|
|
382
|
+
:failure-at="instance?.failure?.occurredAt"
|
|
383
|
+
/>
|
|
384
|
+
<div class="flex items-center gap-2">
|
|
385
|
+
<label
|
|
386
|
+
v-if="awaitingHuman && needsAck"
|
|
387
|
+
class="flex items-center gap-1.5 text-[11px] text-amber-300/90"
|
|
388
|
+
>
|
|
389
|
+
<input v-model="ackDegraded" type="checkbox" class="accent-amber-500" />
|
|
390
|
+
{{ t('visualConfirm.reviewedManually') }}
|
|
391
|
+
</label>
|
|
392
|
+
<UButton
|
|
393
|
+
size="sm"
|
|
394
|
+
variant="soft"
|
|
395
|
+
color="neutral"
|
|
396
|
+
icon="i-lucide-refresh-cw"
|
|
397
|
+
:loading="busy"
|
|
398
|
+
:disabled="busy || !awaitingHuman || !access.canExecuteRuns.value"
|
|
399
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
400
|
+
@click="recapture"
|
|
401
|
+
>
|
|
402
|
+
{{ t('visualConfirm.recapture') }}
|
|
403
|
+
</UButton>
|
|
404
|
+
<UButton
|
|
405
|
+
color="primary"
|
|
406
|
+
icon="i-lucide-circle-check"
|
|
407
|
+
:loading="busy"
|
|
408
|
+
:disabled="!canApprove || !access.canExecuteRuns.value"
|
|
409
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
410
|
+
@click="approve"
|
|
411
|
+
>
|
|
412
|
+
{{ t('visualConfirm.approve') }}
|
|
413
|
+
</UButton>
|
|
414
|
+
</div>
|
|
415
|
+
</footer>
|
|
416
|
+
</ResultWindowShell>
|
|
417
|
+
|
|
418
|
+
<!-- Shared zoom/pan viewer for any screenshot in the gallery — a sibling overlay that layers
|
|
419
|
+
above this window on the shared modal stack while open. -->
|
|
420
|
+
<ArtifactLightbox
|
|
421
|
+
v-model:open="lightboxOpen"
|
|
422
|
+
v-model:index="lightboxIndex"
|
|
423
|
+
:items="lightboxItems"
|
|
424
|
+
:blobs="blobs"
|
|
425
|
+
/>
|
|
464
426
|
</template>
|
|
@@ -44,6 +44,24 @@ describe('inspector panel group', () => {
|
|
|
44
44
|
])
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
+
it('a document frame hides the test-infra / test-credentials / release-health panels', () => {
|
|
48
|
+
// A doc repo stands up no test env and ships no release, so those panels don't apply.
|
|
49
|
+
expect(visibleIds(block('frame', 'document'))).toEqual([
|
|
50
|
+
'container-summary',
|
|
51
|
+
'service-fragments',
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('a library frame still shows the test/deploy panels (only document is excluded)', () => {
|
|
56
|
+
expect(visibleIds(block('frame', 'library'))).toEqual([
|
|
57
|
+
'container-summary',
|
|
58
|
+
'service-test-config',
|
|
59
|
+
'service-test-secrets',
|
|
60
|
+
'service-fragments',
|
|
61
|
+
'service-release-health',
|
|
62
|
+
])
|
|
63
|
+
})
|
|
64
|
+
|
|
47
65
|
it('a frontend frame swaps connections for frontend-config', () => {
|
|
48
66
|
expect(visibleIds(block('frame', 'frontend'))).toEqual([
|
|
49
67
|
'container-summary',
|
|
@@ -75,6 +75,14 @@ const isTask = (b: Block) => b.level === 'task'
|
|
|
75
75
|
const isFrame = (b: Block) => b.level === 'frame'
|
|
76
76
|
/** frame OR module — the "container" panels. */
|
|
77
77
|
const isContainer = (b: Block) => b.level === 'frame' || b.level === 'module'
|
|
78
|
+
/**
|
|
79
|
+
* A frame that gets built, tested and deployed as a running thing — i.e. every frame
|
|
80
|
+
* EXCEPT a `document` repo. A document repo (`FrameRepoType` `document`) only runs
|
|
81
|
+
* doc/spike tasks + doc pipelines: it stands up no test environment, holds no test
|
|
82
|
+
* credentials and never ships a release to watch, so the test-infrastructure /
|
|
83
|
+
* test-credentials / post-release-health panels don't apply to it and are hidden.
|
|
84
|
+
*/
|
|
85
|
+
const isDeployableFrame = (b: Block) => isFrame(b) && b.type !== 'document'
|
|
78
86
|
|
|
79
87
|
/**
|
|
80
88
|
* The built-in panel specs. The `when` predicates and orders reproduce
|
|
@@ -83,7 +91,9 @@ const isContainer = (b: Block) => b.level === 'frame' || b.level === 'module'
|
|
|
83
91
|
* - task body: context docs/issues, then schedule → execution → estimate →
|
|
84
92
|
* dependencies → run-settings → agent-config → structure.
|
|
85
93
|
* - service/module body: container summary (frame|module), then the frame-only
|
|
86
|
-
* panels, with frontend-config / service-connections further gated on `type
|
|
94
|
+
* panels, with frontend-config / service-connections further gated on `type`,
|
|
95
|
+
* and the test-infrastructure / test-credentials / post-release-health panels
|
|
96
|
+
* hidden for a `document` frame (a doc repo has no test env / release to config).
|
|
87
97
|
* - epic / initiative bodies: their single inspector.
|
|
88
98
|
*
|
|
89
99
|
* The frame-only "view requirements" button and the cross-cutting run banners
|
|
@@ -104,10 +114,10 @@ export const INSPECTOR_PANEL_SPECS: readonly InspectorPanelSpec[] = [
|
|
|
104
114
|
{ id: 'container-summary', order: 110, when: isContainer },
|
|
105
115
|
{ id: 'frontend-config', order: 120, when: (b) => isFrame(b) && b.type === 'frontend' },
|
|
106
116
|
{ id: 'service-connections', order: 130, when: (b) => isFrame(b) && b.type === 'service' },
|
|
107
|
-
{ id: 'service-test-config', order: 140, when:
|
|
108
|
-
{ id: 'service-test-secrets', order: 150, when:
|
|
117
|
+
{ id: 'service-test-config', order: 140, when: isDeployableFrame },
|
|
118
|
+
{ id: 'service-test-secrets', order: 150, when: isDeployableFrame },
|
|
109
119
|
{ id: 'service-fragments', order: 160, when: isFrame },
|
|
110
|
-
{ id: 'service-release-health', order: 170, when:
|
|
120
|
+
{ id: 'service-release-health', order: 170, when: isDeployableFrame },
|
|
111
121
|
{ id: 'epic-children', order: 200, when: (b) => b.level === 'epic' },
|
|
112
122
|
{ id: 'initiative-inspector', order: 210, when: (b) => b.level === 'initiative' },
|
|
113
123
|
]
|
package/i18n/locales/de.json
CHANGED
|
@@ -4445,7 +4445,6 @@
|
|
|
4445
4445
|
"footer": "Szenarien sind die Bereiche, die der Tester zum Prüfen gewählt hat (seine Akzeptanzszenarien aus der Spezifikation). Ergebnisse und Bedenken werden nach Name darunter gruppiert."
|
|
4446
4446
|
},
|
|
4447
4447
|
"visualConfirm": {
|
|
4448
|
-
"ariaLabel": "Visuelle Bestätigung",
|
|
4449
4448
|
"title": "Visuelle Bestätigung",
|
|
4450
4449
|
"titleWithBlock": "Visuelle Bestätigung — {title}",
|
|
4451
4450
|
"subtitle": "Die UI mit den Referenzdesigns vergleichen",
|
package/i18n/locales/en.json
CHANGED
package/i18n/locales/es.json
CHANGED
|
@@ -4111,7 +4111,6 @@
|
|
|
4111
4111
|
}
|
|
4112
4112
|
},
|
|
4113
4113
|
"visualConfirm": {
|
|
4114
|
-
"ariaLabel": "Confirmación visual",
|
|
4115
4114
|
"title": "Confirmación visual",
|
|
4116
4115
|
"titleWithBlock": "Confirmación visual — {title}",
|
|
4117
4116
|
"subtitle": "Revisa la interfaz frente a los diseños de referencia",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -4111,7 +4111,6 @@
|
|
|
4111
4111
|
}
|
|
4112
4112
|
},
|
|
4113
4113
|
"visualConfirm": {
|
|
4114
|
-
"ariaLabel": "Confirmation visuelle",
|
|
4115
4114
|
"title": "Confirmation visuelle",
|
|
4116
4115
|
"titleWithBlock": "Confirmation visuelle — {title}",
|
|
4117
4116
|
"subtitle": "Comparez l'interface aux maquettes de référence",
|