@cat-factory/app 0.147.0 → 0.147.2
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/board/nodes/TaskPipelineMini.vue +23 -2
- package/app/components/pipeline/PipelineProgress.vue +25 -1
- package/app/components/prReview/PrReviewPhaseBadge.vue +74 -0
- package/app/components/prReview/PrReviewWindow.vue +458 -361
- package/app/utils/prReviewProgress.spec.ts +122 -0
- package/app/utils/prReviewProgress.ts +79 -0
- package/i18n/locales/de.json +24 -3
- package/i18n/locales/en.json +24 -3
- package/i18n/locales/es.json +24 -3
- package/i18n/locales/fr.json +24 -3
- package/i18n/locales/he.json +24 -3
- package/i18n/locales/it.json +24 -3
- package/i18n/locales/ja.json +24 -3
- package/i18n/locales/pl.json +24 -3
- package/i18n/locales/tr.json +24 -3
- package/i18n/locales/uk.json +24 -3
- package/package.json +1 -1
|
@@ -17,10 +17,13 @@ import type {
|
|
|
17
17
|
PrReviewResolution,
|
|
18
18
|
PrReviewSeverity,
|
|
19
19
|
PrReviewStepState,
|
|
20
|
+
StepSubtaskItem,
|
|
20
21
|
StepSubtasks,
|
|
21
22
|
} from '~/types/execution'
|
|
22
23
|
import { subtaskIconClass } from '~/utils/pipelineRender'
|
|
24
|
+
import { activeChunkLabels, chunkReviewPercent, isSlicingChunks } from '~/utils/prReviewProgress'
|
|
23
25
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
26
|
+
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
24
27
|
|
|
25
28
|
const execution = useExecutionStore()
|
|
26
29
|
const board = useBoardStore()
|
|
@@ -51,17 +54,46 @@ const awaiting = computed(() => status.value === 'awaiting_selection')
|
|
|
51
54
|
// selection controls + per-finding actions are disabled until the verdict lands and it re-parks.
|
|
52
55
|
const challenging = computed(() => status.value === 'challenging')
|
|
53
56
|
// The reviewer's live todo list while it works, streamed onto the step. Its entries are the
|
|
54
|
-
// cohesive slices/chunks the agent grouped the diff into (plus a final "aggregate" step)
|
|
55
|
-
//
|
|
57
|
+
// cohesive slices/chunks the agent grouped the diff into (plus a final "aggregate" step). The
|
|
58
|
+
// derivation of the two `reviewing`-phase sub-states from it lives in `~/utils/prReviewProgress`
|
|
59
|
+
// (pure + unit-tested); see that module for the slicing-vs-reviewing signal.
|
|
56
60
|
const subtasks = computed<StepSubtasks | null>(() => step.value?.subtasks ?? null)
|
|
57
|
-
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Slicing sub-phase: the reviewer is still grouping the diff into chunks (no todo list yet).
|
|
64
|
+
* Once the list exists, slicing is done and we render the per-chunk status list instead.
|
|
65
|
+
*/
|
|
66
|
+
const slicing = computed(() => status.value === 'reviewing' && isSlicingChunks(subtasks.value))
|
|
67
|
+
|
|
68
|
+
/** Chunk-review completion, clamped 0..100 for the progress bar. */
|
|
69
|
+
const chunkPercent = computed(() => chunkReviewPercent(subtasks.value))
|
|
70
|
+
|
|
71
|
+
/** The chunks the reviewer is actively working through right now (their labels), for the callout. */
|
|
72
|
+
const activeChunks = computed<string[]>(() => activeChunkLabels(subtasks.value))
|
|
58
73
|
|
|
59
74
|
/** Icon per todo-item status (matches the pipeline timeline's live subtask breakdown). */
|
|
60
|
-
const ITEM_ICON: Record<
|
|
75
|
+
const ITEM_ICON: Record<StepSubtaskItem['status'], string> = {
|
|
61
76
|
completed: 'i-lucide-check-circle-2',
|
|
62
77
|
in_progress: 'i-lucide-loader-circle',
|
|
63
78
|
pending: 'i-lucide-circle',
|
|
64
79
|
}
|
|
80
|
+
|
|
81
|
+
// Per-chunk status label + chip styling. The key map is an exhaustive Record over the subtask
|
|
82
|
+
// status union, so adding a status without a label fails the typecheck (the sanctioned dynamic
|
|
83
|
+
// enum→key pattern — tier 1 can't see a runtime-built key).
|
|
84
|
+
const CHUNK_STATUS_KEY: Record<StepSubtaskItem['status'], string> = {
|
|
85
|
+
completed: 'prReview.reviewing.chunkStatus.completed',
|
|
86
|
+
in_progress: 'prReview.reviewing.chunkStatus.in_progress',
|
|
87
|
+
pending: 'prReview.reviewing.chunkStatus.pending',
|
|
88
|
+
}
|
|
89
|
+
const CHUNK_STATUS_CLASS: Record<StepSubtaskItem['status'], string> = {
|
|
90
|
+
completed: 'bg-emerald-500/15 text-emerald-300',
|
|
91
|
+
in_progress: 'bg-indigo-500/15 text-indigo-300',
|
|
92
|
+
pending: 'bg-slate-700/60 text-slate-400',
|
|
93
|
+
}
|
|
94
|
+
function chunkStatusLabel(status: StepSubtaskItem['status']): string {
|
|
95
|
+
return t(CHUNK_STATUS_KEY[status])
|
|
96
|
+
}
|
|
65
97
|
// A resolution is executing (the Fixer is committing, or comments are being posted) — show a
|
|
66
98
|
// working state between the human's choice and the run advancing/the stream echoing `done`.
|
|
67
99
|
const working = computed(() => status.value === 'fixing' || status.value === 'posting')
|
|
@@ -222,403 +254,468 @@ async function onDismiss(id: string): Promise<void> {
|
|
|
222
254
|
</a>
|
|
223
255
|
</template>
|
|
224
256
|
|
|
225
|
-
<div class="min-h-0 flex-1
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<div
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<span data-testid="pr-review-chunk-count">
|
|
247
|
-
{{
|
|
248
|
-
t('prReview.reviewing.chunks', {
|
|
249
|
-
completed: subtasks!.completed,
|
|
250
|
-
total: subtasks!.total,
|
|
251
|
-
})
|
|
252
|
-
}}
|
|
253
|
-
</span>
|
|
254
|
-
<span v-if="subtasks!.inProgress > 0" class="text-indigo-300">
|
|
255
|
-
{{ t('prReview.reviewing.inProgress', { count: subtasks!.inProgress }) }}
|
|
256
|
-
</span>
|
|
257
|
-
</div>
|
|
258
|
-
<div class="mt-1 h-1.5 overflow-hidden rounded-full bg-slate-700/60">
|
|
259
|
-
<div
|
|
260
|
-
class="h-full rounded-full bg-indigo-400 transition-all duration-500"
|
|
261
|
-
:style="{ width: `${(subtasks!.completed / subtasks!.total) * 100}%` }"
|
|
262
|
-
/>
|
|
257
|
+
<div class="flex min-h-0 flex-1">
|
|
258
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
259
|
+
<!-- Reviewing: the read-only reviewer is still working. The phase is told apart precisely —
|
|
260
|
+
SLICING (still grouping the diff into chunks, no plan yet) vs REVIEWING (slicing done,
|
|
261
|
+
working through the chunks) — so the copy never claims "reviewing" while it's slicing. -->
|
|
262
|
+
<div
|
|
263
|
+
v-if="status === 'reviewing'"
|
|
264
|
+
data-testid="pr-review-reviewing"
|
|
265
|
+
class="flex h-full flex-col"
|
|
266
|
+
>
|
|
267
|
+
<!-- SLICING: no todo list yet — the reviewer is still grouping the diff into chunks. -->
|
|
268
|
+
<div
|
|
269
|
+
v-if="slicing"
|
|
270
|
+
data-testid="pr-review-slicing"
|
|
271
|
+
class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
272
|
+
>
|
|
273
|
+
<UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
|
|
274
|
+
<p class="text-sm text-slate-200">{{ t('prReview.reviewing.slicing.title') }}</p>
|
|
275
|
+
<p class="max-w-sm text-[11px] text-slate-500">
|
|
276
|
+
{{ t('prReview.reviewing.slicing.hint') }}
|
|
277
|
+
</p>
|
|
263
278
|
</div>
|
|
264
279
|
|
|
265
|
-
<!--
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
class="mt-3 space-y-1.5"
|
|
269
|
-
data-testid="pr-review-chunks"
|
|
270
|
-
>
|
|
271
|
-
<li
|
|
272
|
-
v-for="(item, i) in subtasks!.items"
|
|
273
|
-
:key="i"
|
|
274
|
-
class="flex items-start gap-1.5 text-[12px]"
|
|
275
|
-
:class="
|
|
276
|
-
item.status === 'completed'
|
|
277
|
-
? 'text-slate-500 line-through'
|
|
278
|
-
: item.status === 'in_progress'
|
|
279
|
-
? 'text-slate-100'
|
|
280
|
-
: 'text-slate-400'
|
|
281
|
-
"
|
|
282
|
-
>
|
|
280
|
+
<!-- REVIEWING: slicing is done — show every chunk with its status + which are active now. -->
|
|
281
|
+
<div v-else data-testid="pr-review-reviewing-chunks" class="py-2">
|
|
282
|
+
<div class="mb-1 flex items-center gap-2 text-sm text-slate-200">
|
|
283
283
|
<UIcon
|
|
284
|
-
|
|
285
|
-
class="
|
|
286
|
-
|
|
284
|
+
name="i-lucide-loader-circle"
|
|
285
|
+
class="h-4 w-4 shrink-0 animate-spin text-indigo-300"
|
|
286
|
+
/>
|
|
287
|
+
<span>{{ t('prReview.reviewing.reviewingChunks.title') }}</span>
|
|
288
|
+
</div>
|
|
289
|
+
<p class="mb-3 text-[11px] text-slate-500">
|
|
290
|
+
{{ t('prReview.reviewing.reviewingChunks.hint') }}
|
|
291
|
+
</p>
|
|
292
|
+
|
|
293
|
+
<div class="flex items-center justify-between text-[11px] text-slate-400">
|
|
294
|
+
<span data-testid="pr-review-chunk-count">
|
|
295
|
+
{{
|
|
296
|
+
t('prReview.reviewing.chunks', {
|
|
297
|
+
completed: subtasks!.completed,
|
|
298
|
+
total: subtasks!.total,
|
|
299
|
+
})
|
|
300
|
+
}}
|
|
301
|
+
</span>
|
|
302
|
+
<span v-if="subtasks!.inProgress > 0" class="text-indigo-300">
|
|
303
|
+
{{ t('prReview.reviewing.inProgress', { count: subtasks!.inProgress }) }}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="mt-1 h-1.5 overflow-hidden rounded-full bg-slate-700/60">
|
|
307
|
+
<div
|
|
308
|
+
class="h-full rounded-full bg-indigo-400 transition-all duration-500"
|
|
309
|
+
:style="{ width: `${chunkPercent}%` }"
|
|
287
310
|
/>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<!-- The chunk(s) being actively reviewed right now, called out on their own. -->
|
|
314
|
+
<div
|
|
315
|
+
v-if="activeChunks.length"
|
|
316
|
+
data-testid="pr-review-active-chunks"
|
|
317
|
+
class="mt-3 rounded-lg border border-indigo-500/30 bg-indigo-500/5 px-2.5 py-2"
|
|
318
|
+
>
|
|
319
|
+
<p class="mb-1 text-[10px] font-semibold uppercase tracking-wide text-indigo-300">
|
|
320
|
+
{{ t('prReview.reviewing.activeHeading') }}
|
|
321
|
+
</p>
|
|
322
|
+
<ul class="space-y-1">
|
|
323
|
+
<li
|
|
324
|
+
v-for="(label, i) in activeChunks"
|
|
325
|
+
:key="i"
|
|
326
|
+
class="flex items-start gap-1.5 text-[12px] text-slate-100"
|
|
327
|
+
>
|
|
328
|
+
<UIcon
|
|
329
|
+
name="i-lucide-loader-circle"
|
|
330
|
+
class="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin text-indigo-300"
|
|
331
|
+
/>
|
|
332
|
+
<span class="min-w-0">{{ label }}</span>
|
|
333
|
+
</li>
|
|
334
|
+
</ul>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<!-- Every chunk with its explicit status (Reviewed / Reviewing… / Queued). -->
|
|
338
|
+
<template v-if="subtasks!.items?.length">
|
|
339
|
+
<p
|
|
340
|
+
class="mb-1.5 mt-3 text-[10px] font-semibold uppercase tracking-wide text-slate-400"
|
|
341
|
+
>
|
|
342
|
+
{{ t('prReview.reviewing.chunksHeading') }}
|
|
343
|
+
</p>
|
|
344
|
+
<ul class="space-y-1.5" data-testid="pr-review-chunks">
|
|
345
|
+
<li
|
|
346
|
+
v-for="(item, i) in subtasks!.items"
|
|
347
|
+
:key="i"
|
|
348
|
+
data-testid="pr-review-chunk"
|
|
349
|
+
class="flex items-center gap-1.5 text-[12px]"
|
|
350
|
+
:class="
|
|
351
|
+
item.status === 'completed'
|
|
352
|
+
? 'text-slate-500'
|
|
353
|
+
: item.status === 'in_progress'
|
|
354
|
+
? 'text-slate-100'
|
|
355
|
+
: 'text-slate-400'
|
|
356
|
+
"
|
|
357
|
+
>
|
|
358
|
+
<UIcon
|
|
359
|
+
:name="ITEM_ICON[item.status]"
|
|
360
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
361
|
+
:class="subtaskIconClass(item.status, false)"
|
|
362
|
+
/>
|
|
363
|
+
<span
|
|
364
|
+
class="min-w-0 flex-1 truncate"
|
|
365
|
+
:class="item.status === 'completed' ? 'line-through' : ''"
|
|
366
|
+
>
|
|
367
|
+
{{ item.label }}
|
|
368
|
+
</span>
|
|
369
|
+
<span
|
|
370
|
+
data-testid="pr-review-chunk-status"
|
|
371
|
+
class="shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium uppercase"
|
|
372
|
+
:class="CHUNK_STATUS_CLASS[item.status]"
|
|
373
|
+
>
|
|
374
|
+
{{ chunkStatusLabel(item.status) }}
|
|
375
|
+
</span>
|
|
376
|
+
</li>
|
|
377
|
+
</ul>
|
|
378
|
+
</template>
|
|
379
|
+
</div>
|
|
291
380
|
</div>
|
|
292
381
|
|
|
293
|
-
<!--
|
|
382
|
+
<!-- A resolution is executing: the Fixer is committing / comments are being posted. -->
|
|
294
383
|
<div
|
|
295
|
-
v-else
|
|
384
|
+
v-else-if="working"
|
|
385
|
+
data-testid="pr-review-working"
|
|
296
386
|
class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
297
387
|
>
|
|
298
388
|
<UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
|
|
299
|
-
<p class="text-sm">
|
|
300
|
-
|
|
389
|
+
<p class="text-sm">
|
|
390
|
+
{{ status === 'fixing' ? t('prReview.fixing.title') : t('prReview.posting.title') }}
|
|
391
|
+
</p>
|
|
392
|
+
<p class="max-w-sm text-[11px] text-slate-500">
|
|
393
|
+
{{ status === 'fixing' ? t('prReview.fixing.hint') : t('prReview.posting.hint') }}
|
|
394
|
+
</p>
|
|
301
395
|
</div>
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
<!-- A resolution is executing: the Fixer is committing / comments are being posted. -->
|
|
305
|
-
<div
|
|
306
|
-
v-else-if="working"
|
|
307
|
-
data-testid="pr-review-working"
|
|
308
|
-
class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
309
|
-
>
|
|
310
|
-
<UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
|
|
311
|
-
<p class="text-sm">
|
|
312
|
-
{{ status === 'fixing' ? t('prReview.fixing.title') : t('prReview.posting.title') }}
|
|
313
|
-
</p>
|
|
314
|
-
<p class="max-w-sm text-[11px] text-slate-500">
|
|
315
|
-
{{ status === 'fixing' ? t('prReview.fixing.hint') : t('prReview.posting.hint') }}
|
|
316
|
-
</p>
|
|
317
|
-
</div>
|
|
318
396
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
397
|
+
<template v-else>
|
|
398
|
+
<p
|
|
399
|
+
v-if="prReview.error"
|
|
400
|
+
class="mb-3 rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
|
|
401
|
+
>
|
|
402
|
+
{{ prReview.error }}
|
|
403
|
+
</p>
|
|
326
404
|
|
|
327
|
-
|
|
405
|
+
<!-- The outcome of the most recent `post` attempt: how many of how many comments landed,
|
|
328
406
|
which failed + why, and how many findings were folded into the summary because their
|
|
329
407
|
line isn't in the PR diff. Surfaced so a partial/failed post is legible + retryable. -->
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
408
|
+
<div
|
|
409
|
+
v-if="postReport"
|
|
410
|
+
data-testid="pr-review-post-report"
|
|
411
|
+
class="mb-3 rounded-lg border px-3 py-2 text-[12px]"
|
|
412
|
+
:class="
|
|
413
|
+
postReport.failures.length > 0 || postReport.bodyPosted === false
|
|
414
|
+
? 'border-amber-500/40 bg-amber-500/10 text-amber-200'
|
|
415
|
+
: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-200'
|
|
416
|
+
"
|
|
417
|
+
>
|
|
418
|
+
<div class="mb-1 flex items-center gap-1.5 font-medium">
|
|
419
|
+
<UIcon
|
|
420
|
+
:name="
|
|
421
|
+
postReport.failures.length > 0 || postReport.bodyPosted === false
|
|
422
|
+
? 'i-lucide-alert-triangle'
|
|
423
|
+
: 'i-lucide-check-circle-2'
|
|
424
|
+
"
|
|
425
|
+
class="h-4 w-4 shrink-0"
|
|
426
|
+
/>
|
|
427
|
+
<span>{{ t('prReview.postReport.heading') }}</span>
|
|
428
|
+
</div>
|
|
429
|
+
<p data-testid="pr-review-post-count">
|
|
430
|
+
{{
|
|
431
|
+
t('prReview.postReport.posted', {
|
|
432
|
+
posted: postReport.posted,
|
|
433
|
+
attempted: postReport.attempted,
|
|
434
|
+
})
|
|
435
|
+
}}
|
|
436
|
+
</p>
|
|
437
|
+
<p v-if="postReport.folded > 0" class="mt-0.5 opacity-90">
|
|
438
|
+
{{ t('prReview.postReport.folded', { count: postReport.folded }) }}
|
|
439
|
+
</p>
|
|
440
|
+
<template v-if="postReport.failures.length > 0">
|
|
441
|
+
<p class="mt-1.5 font-medium">{{ t('prReview.postReport.failuresHeading') }}</p>
|
|
442
|
+
<ul class="mt-0.5 space-y-0.5" data-testid="pr-review-post-failures">
|
|
443
|
+
<li v-for="f in postReport.failures" :key="f.findingId" class="flex gap-1.5">
|
|
444
|
+
<code class="shrink-0 text-amber-100"
|
|
445
|
+
>{{ f.path }}<template v-if="f.line != null">:{{ f.line }}</template></code
|
|
446
|
+
>
|
|
447
|
+
<span class="opacity-90">— {{ f.reason }}</span>
|
|
448
|
+
</li>
|
|
449
|
+
</ul>
|
|
450
|
+
</template>
|
|
451
|
+
<p v-if="postReport.bodyPosted === false && postReport.bodyError" class="mt-1.5">
|
|
452
|
+
{{ t('prReview.postReport.bodyError', { error: postReport.bodyError }) }}
|
|
453
|
+
</p>
|
|
350
454
|
</div>
|
|
351
|
-
<p data-testid="pr-review-post-count">
|
|
352
|
-
{{
|
|
353
|
-
t('prReview.postReport.posted', {
|
|
354
|
-
posted: postReport.posted,
|
|
355
|
-
attempted: postReport.attempted,
|
|
356
|
-
})
|
|
357
|
-
}}
|
|
358
|
-
</p>
|
|
359
|
-
<p v-if="postReport.folded > 0" class="mt-0.5 opacity-90">
|
|
360
|
-
{{ t('prReview.postReport.folded', { count: postReport.folded }) }}
|
|
361
|
-
</p>
|
|
362
|
-
<template v-if="postReport.failures.length > 0">
|
|
363
|
-
<p class="mt-1.5 font-medium">{{ t('prReview.postReport.failuresHeading') }}</p>
|
|
364
|
-
<ul class="mt-0.5 space-y-0.5" data-testid="pr-review-post-failures">
|
|
365
|
-
<li v-for="f in postReport.failures" :key="f.findingId" class="flex gap-1.5">
|
|
366
|
-
<code class="shrink-0 text-amber-100"
|
|
367
|
-
>{{ f.path }}<template v-if="f.line != null">:{{ f.line }}</template></code
|
|
368
|
-
>
|
|
369
|
-
<span class="opacity-90">— {{ f.reason }}</span>
|
|
370
|
-
</li>
|
|
371
|
-
</ul>
|
|
372
|
-
</template>
|
|
373
|
-
<p v-if="postReport.bodyPosted === false && postReport.bodyError" class="mt-1.5">
|
|
374
|
-
{{ t('prReview.postReport.bodyError', { error: postReport.bodyError }) }}
|
|
375
|
-
</p>
|
|
376
|
-
</div>
|
|
377
|
-
|
|
378
|
-
<!-- The reviewer's overall assessment. -->
|
|
379
|
-
<p
|
|
380
|
-
v-if="state?.summary"
|
|
381
|
-
class="mb-3 rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
|
|
382
|
-
>
|
|
383
|
-
<span class="text-slate-500">{{ t('prReview.summaryLabel') }}</span>
|
|
384
|
-
{{ state.summary }}
|
|
385
|
-
</p>
|
|
386
455
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
456
|
+
<!-- The reviewer's overall assessment. -->
|
|
457
|
+
<p
|
|
458
|
+
v-if="state?.summary"
|
|
459
|
+
class="mb-3 rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
|
|
460
|
+
>
|
|
461
|
+
<span class="text-slate-500">{{ t('prReview.summaryLabel') }}</span>
|
|
462
|
+
{{ state.summary }}
|
|
463
|
+
</p>
|
|
394
464
|
|
|
395
|
-
|
|
396
|
-
<!-- A challenge is in flight: the Challenge Investigator is re-examining a finding. -->
|
|
465
|
+
<!-- A clean PR / resolved review with no findings. -->
|
|
397
466
|
<div
|
|
398
|
-
v-if="
|
|
399
|
-
|
|
400
|
-
class="mb-3 flex items-center gap-2 rounded-md border border-indigo-500/40 bg-indigo-500/10 px-3 py-2 text-[12px] text-indigo-200"
|
|
467
|
+
v-if="findings.length === 0"
|
|
468
|
+
class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-6 text-center text-[13px] text-slate-300"
|
|
401
469
|
>
|
|
402
|
-
|
|
403
|
-
<span>{{ t('prReview.challenge.investigatingBanner') }}</span>
|
|
470
|
+
{{ t('prReview.noFindings') }}
|
|
404
471
|
</div>
|
|
405
472
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
<
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
{{ t('prReview.selectAll') }}
|
|
413
|
-
</button>
|
|
414
|
-
<button class="text-indigo-300 hover:underline" @click="clearAll">
|
|
415
|
-
{{ t('prReview.clear') }}
|
|
416
|
-
</button>
|
|
417
|
-
</div>
|
|
418
|
-
|
|
419
|
-
<!-- Findings grouped by slice -->
|
|
420
|
-
<section v-for="g in groups" :key="g.id" class="mb-4">
|
|
421
|
-
<h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
422
|
-
{{ g.title }}
|
|
423
|
-
</h3>
|
|
424
|
-
<p v-if="g.rationale" class="mb-1.5 text-[11px] text-slate-500">
|
|
425
|
-
{{ g.rationale }}
|
|
426
|
-
</p>
|
|
427
|
-
<article
|
|
428
|
-
v-for="f in g.items"
|
|
429
|
-
:key="f.id"
|
|
430
|
-
data-testid="pr-review-finding"
|
|
431
|
-
class="mb-1.5 rounded-xl border px-3 py-2 transition"
|
|
432
|
-
:class="[
|
|
433
|
-
awaiting && selected.has(f.id) && !isRetracted(f)
|
|
434
|
-
? 'border-indigo-500/60 bg-indigo-500/5'
|
|
435
|
-
: 'border-slate-800 bg-slate-900/60',
|
|
436
|
-
isRetracted(f) ? 'opacity-60' : '',
|
|
437
|
-
]"
|
|
473
|
+
<template v-else>
|
|
474
|
+
<!-- A challenge is in flight: the Challenge Investigator is re-examining a finding. -->
|
|
475
|
+
<div
|
|
476
|
+
v-if="challenging"
|
|
477
|
+
data-testid="pr-review-challenging"
|
|
478
|
+
class="mb-3 flex items-center gap-2 rounded-md border border-indigo-500/40 bg-indigo-500/10 px-3 py-2 text-[12px] text-indigo-200"
|
|
438
479
|
>
|
|
439
|
-
<
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
480
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 shrink-0 animate-spin" />
|
|
481
|
+
<span>{{ t('prReview.challenge.investigatingBanner') }}</span>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
<!-- Selection toolbar -->
|
|
485
|
+
<div v-if="awaiting" class="mb-2 flex items-center gap-3 text-[11px] text-slate-400">
|
|
486
|
+
<span data-testid="pr-review-selected-count">
|
|
487
|
+
{{ t('prReview.selectedCount', { count: activeSelectedIds.length }) }}
|
|
488
|
+
</span>
|
|
489
|
+
<button class="text-indigo-300 hover:underline" @click="selectAll">
|
|
490
|
+
{{ t('prReview.selectAll') }}
|
|
491
|
+
</button>
|
|
492
|
+
<button class="text-indigo-300 hover:underline" @click="clearAll">
|
|
493
|
+
{{ t('prReview.clear') }}
|
|
494
|
+
</button>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<!-- Findings grouped by slice -->
|
|
498
|
+
<section v-for="g in groups" :key="g.id" class="mb-4">
|
|
499
|
+
<h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
500
|
+
{{ g.title }}
|
|
501
|
+
</h3>
|
|
502
|
+
<p v-if="g.rationale" class="mb-1.5 text-[11px] text-slate-500">
|
|
503
|
+
{{ g.rationale }}
|
|
504
|
+
</p>
|
|
505
|
+
<article
|
|
506
|
+
v-for="f in g.items"
|
|
507
|
+
:key="f.id"
|
|
508
|
+
data-testid="pr-review-finding"
|
|
509
|
+
class="mb-1.5 rounded-xl border px-3 py-2 transition"
|
|
510
|
+
:class="[
|
|
511
|
+
awaiting && selected.has(f.id) && !isRetracted(f)
|
|
512
|
+
? 'border-indigo-500/60 bg-indigo-500/5'
|
|
513
|
+
: 'border-slate-800 bg-slate-900/60',
|
|
514
|
+
isRetracted(f) ? 'opacity-60' : '',
|
|
515
|
+
]"
|
|
516
|
+
>
|
|
517
|
+
<div class="flex items-start gap-2">
|
|
518
|
+
<input
|
|
519
|
+
v-if="awaiting || challenging"
|
|
520
|
+
type="checkbox"
|
|
521
|
+
class="mt-1 accent-indigo-500"
|
|
522
|
+
data-testid="pr-review-finding-toggle"
|
|
523
|
+
:checked="selected.has(f.id) && !isRetracted(f)"
|
|
524
|
+
:disabled="!awaiting || isRetracted(f)"
|
|
525
|
+
@change="toggle(f.id)"
|
|
526
|
+
/>
|
|
527
|
+
<div class="min-w-0 flex-1">
|
|
528
|
+
<div class="flex flex-wrap items-center gap-1.5">
|
|
529
|
+
<span
|
|
530
|
+
class="rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase ring-1"
|
|
531
|
+
:class="SEVERITY_CLASS[f.severity]"
|
|
532
|
+
>
|
|
533
|
+
{{ t(`prReview.severity.${f.severity}`) }}
|
|
534
|
+
</span>
|
|
535
|
+
<span class="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-300">
|
|
536
|
+
{{ t(`prReview.category.${f.category}`) }}
|
|
537
|
+
</span>
|
|
538
|
+
<span
|
|
539
|
+
v-if="postedIds.has(f.id)"
|
|
540
|
+
data-testid="pr-review-finding-posted"
|
|
541
|
+
class="rounded bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-emerald-300 ring-1 ring-emerald-500/30"
|
|
542
|
+
>
|
|
543
|
+
{{ t('prReview.postReport.postedBadge') }}
|
|
544
|
+
</span>
|
|
545
|
+
<!-- Challenge outcome badges -->
|
|
546
|
+
<span
|
|
547
|
+
v-if="isRetracted(f)"
|
|
548
|
+
data-testid="pr-review-finding-retracted"
|
|
549
|
+
class="rounded bg-rose-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-rose-300 ring-1 ring-rose-500/30"
|
|
550
|
+
>
|
|
551
|
+
{{ t('prReview.challenge.retractedBadge') }}
|
|
552
|
+
</span>
|
|
553
|
+
<span
|
|
554
|
+
v-else-if="isAmended(f)"
|
|
555
|
+
data-testid="pr-review-finding-amended"
|
|
556
|
+
class="rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-sky-300 ring-1 ring-sky-500/30"
|
|
557
|
+
>
|
|
558
|
+
{{ t('prReview.challenge.strengthenedBadge') }}
|
|
559
|
+
</span>
|
|
560
|
+
<span
|
|
561
|
+
v-else-if="isUpheld(f)"
|
|
562
|
+
data-testid="pr-review-finding-upheld"
|
|
563
|
+
class="rounded bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-emerald-300 ring-1 ring-emerald-500/30"
|
|
564
|
+
>
|
|
565
|
+
{{ t('prReview.challenge.upheldBadge') }}
|
|
566
|
+
</span>
|
|
567
|
+
<span
|
|
568
|
+
v-else-if="isChallengeFailed(f)"
|
|
569
|
+
data-testid="pr-review-finding-challenge-failed"
|
|
570
|
+
class="rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-amber-300 ring-1 ring-amber-500/30"
|
|
571
|
+
>
|
|
572
|
+
{{ t('prReview.challenge.failedBadge') }}
|
|
573
|
+
</span>
|
|
574
|
+
<span
|
|
575
|
+
v-else-if="isInvestigating(f)"
|
|
576
|
+
data-testid="pr-review-finding-investigating"
|
|
577
|
+
class="flex items-center gap-1 rounded bg-indigo-500/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-indigo-300 ring-1 ring-indigo-500/30"
|
|
578
|
+
>
|
|
579
|
+
<UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
|
|
580
|
+
{{ t('prReview.challenge.investigatingBadge') }}
|
|
581
|
+
</span>
|
|
582
|
+
<h4
|
|
583
|
+
class="min-w-0 flex-1 text-[13px] font-medium text-slate-100"
|
|
584
|
+
:class="isRetracted(f) ? 'line-through' : ''"
|
|
585
|
+
>
|
|
586
|
+
{{ f.title }}
|
|
587
|
+
</h4>
|
|
588
|
+
</div>
|
|
589
|
+
<p class="mt-0.5 text-[11px] text-slate-500">
|
|
590
|
+
{{ f.path
|
|
591
|
+
}}<template v-if="f.line != null">
|
|
592
|
+
· {{ t('prReview.line', { line: f.line }) }}</template
|
|
593
|
+
>
|
|
594
|
+
</p>
|
|
595
|
+
<p
|
|
596
|
+
class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300"
|
|
506
597
|
:class="isRetracted(f) ? 'line-through' : ''"
|
|
507
598
|
>
|
|
508
|
-
{{ f.
|
|
509
|
-
</
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}}<template v-if="f.line != null">
|
|
514
|
-
· {{ t('prReview.line', { line: f.line }) }}</template
|
|
599
|
+
{{ f.detail }}
|
|
600
|
+
</p>
|
|
601
|
+
<p
|
|
602
|
+
v-if="f.suggestedFix"
|
|
603
|
+
class="mt-1 whitespace-pre-wrap rounded-md bg-slate-800/50 px-2 py-1 text-[11px] text-slate-300"
|
|
515
604
|
>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
:class="isRetracted(f) ? 'line-through' : ''"
|
|
520
|
-
>
|
|
521
|
-
{{ f.detail }}
|
|
522
|
-
</p>
|
|
523
|
-
<p
|
|
524
|
-
v-if="f.suggestedFix"
|
|
525
|
-
class="mt-1 whitespace-pre-wrap rounded-md bg-slate-800/50 px-2 py-1 text-[11px] text-slate-300"
|
|
526
|
-
>
|
|
527
|
-
<span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
|
|
528
|
-
{{ f.suggestedFix }}
|
|
529
|
-
</p>
|
|
605
|
+
<span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
|
|
606
|
+
{{ f.suggestedFix }}
|
|
607
|
+
</p>
|
|
530
608
|
|
|
531
|
-
|
|
609
|
+
<!-- The investigator's justification (why the finding holds up / was retracted),
|
|
532
610
|
or the reason the challenge investigation failed. -->
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
>
|
|
545
|
-
<span class="font-medium">{{
|
|
546
|
-
isChallengeFailed(f)
|
|
547
|
-
? t('prReview.challenge.failedLabel')
|
|
548
|
-
: t('prReview.challenge.verdictLabel')
|
|
549
|
-
}}</span>
|
|
550
|
-
{{ f.challenge.justification }}
|
|
551
|
-
</p>
|
|
552
|
-
|
|
553
|
-
<!-- Per-finding actions: Challenge + Dismiss (only while awaiting a selection). -->
|
|
554
|
-
<div
|
|
555
|
-
v-if="awaiting && !isInvestigating(f)"
|
|
556
|
-
class="mt-1.5 flex items-center gap-3 text-[11px]"
|
|
557
|
-
>
|
|
558
|
-
<button
|
|
559
|
-
v-if="!isRetracted(f)"
|
|
560
|
-
data-testid="pr-review-finding-challenge"
|
|
561
|
-
class="flex items-center gap-1 text-indigo-300 hover:underline disabled:opacity-50"
|
|
562
|
-
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
563
|
-
@click="openChallenge(f.id)"
|
|
564
|
-
>
|
|
565
|
-
<UIcon name="i-lucide-gavel" class="h-3.5 w-3.5" />
|
|
566
|
-
{{
|
|
567
|
-
f.challenge
|
|
568
|
-
? t('prReview.challenge.reChallenge')
|
|
569
|
-
: t('prReview.challenge.action')
|
|
570
|
-
}}
|
|
571
|
-
</button>
|
|
572
|
-
<button
|
|
573
|
-
data-testid="pr-review-finding-dismiss"
|
|
574
|
-
class="flex items-center gap-1 text-slate-400 hover:text-rose-300 hover:underline disabled:opacity-50"
|
|
575
|
-
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
576
|
-
@click="onDismiss(f.id)"
|
|
611
|
+
<p
|
|
612
|
+
v-if="f.challenge?.justification"
|
|
613
|
+
data-testid="pr-review-finding-justification"
|
|
614
|
+
class="mt-1.5 whitespace-pre-wrap rounded-md px-2 py-1 text-[11px]"
|
|
615
|
+
:class="
|
|
616
|
+
isRetracted(f)
|
|
617
|
+
? 'bg-rose-500/10 text-rose-200'
|
|
618
|
+
: isChallengeFailed(f)
|
|
619
|
+
? 'bg-amber-500/10 text-amber-200'
|
|
620
|
+
: 'bg-sky-500/10 text-sky-200'
|
|
621
|
+
"
|
|
577
622
|
>
|
|
578
|
-
<
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
<div
|
|
585
|
-
v-if="challengeForId === f.id"
|
|
586
|
-
data-testid="pr-review-challenge-box"
|
|
587
|
-
class="mt-2 rounded-md border border-indigo-500/40 bg-slate-900/80 p-2"
|
|
588
|
-
>
|
|
589
|
-
<textarea
|
|
590
|
-
v-model="challengeText"
|
|
591
|
-
data-testid="pr-review-challenge-input"
|
|
592
|
-
rows="2"
|
|
593
|
-
:placeholder="t('prReview.challenge.placeholder')"
|
|
594
|
-
class="w-full resize-y rounded border border-slate-700 bg-slate-950/60 px-2 py-1 text-[12px] text-slate-200 outline-none focus:border-indigo-500"
|
|
595
|
-
/>
|
|
596
|
-
<p class="mt-1 text-[10px] text-slate-500">
|
|
597
|
-
{{ t('prReview.challenge.hint') }}
|
|
623
|
+
<span class="font-medium">{{
|
|
624
|
+
isChallengeFailed(f)
|
|
625
|
+
? t('prReview.challenge.failedLabel')
|
|
626
|
+
: t('prReview.challenge.verdictLabel')
|
|
627
|
+
}}</span>
|
|
628
|
+
{{ f.challenge.justification }}
|
|
598
629
|
</p>
|
|
599
|
-
|
|
630
|
+
|
|
631
|
+
<!-- Per-finding actions: Challenge + Dismiss (only while awaiting a selection). -->
|
|
632
|
+
<div
|
|
633
|
+
v-if="awaiting && !isInvestigating(f)"
|
|
634
|
+
class="mt-1.5 flex items-center gap-3 text-[11px]"
|
|
635
|
+
>
|
|
600
636
|
<button
|
|
601
|
-
|
|
602
|
-
|
|
637
|
+
v-if="!isRetracted(f)"
|
|
638
|
+
data-testid="pr-review-finding-challenge"
|
|
639
|
+
class="flex items-center gap-1 text-indigo-300 hover:underline disabled:opacity-50"
|
|
640
|
+
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
641
|
+
@click="openChallenge(f.id)"
|
|
603
642
|
>
|
|
604
|
-
|
|
643
|
+
<UIcon name="i-lucide-gavel" class="h-3.5 w-3.5" />
|
|
644
|
+
{{
|
|
645
|
+
f.challenge
|
|
646
|
+
? t('prReview.challenge.reChallenge')
|
|
647
|
+
: t('prReview.challenge.action')
|
|
648
|
+
}}
|
|
605
649
|
</button>
|
|
606
650
|
<button
|
|
607
|
-
data-testid="pr-review-
|
|
608
|
-
class="
|
|
609
|
-
:disabled="!canResolve"
|
|
610
|
-
@click="
|
|
651
|
+
data-testid="pr-review-finding-dismiss"
|
|
652
|
+
class="flex items-center gap-1 text-slate-400 hover:text-rose-300 hover:underline disabled:opacity-50"
|
|
653
|
+
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
654
|
+
@click="onDismiss(f.id)"
|
|
611
655
|
>
|
|
612
|
-
|
|
656
|
+
<UIcon name="i-lucide-trash-2" class="h-3.5 w-3.5" />
|
|
657
|
+
{{ t('prReview.challenge.dismiss') }}
|
|
613
658
|
</button>
|
|
614
659
|
</div>
|
|
660
|
+
|
|
661
|
+
<!-- The inline challenge box: an OPTIONAL specific concern for the investigator. -->
|
|
662
|
+
<div
|
|
663
|
+
v-if="challengeForId === f.id"
|
|
664
|
+
data-testid="pr-review-challenge-box"
|
|
665
|
+
class="mt-2 rounded-md border border-indigo-500/40 bg-slate-900/80 p-2"
|
|
666
|
+
>
|
|
667
|
+
<textarea
|
|
668
|
+
v-model="challengeText"
|
|
669
|
+
data-testid="pr-review-challenge-input"
|
|
670
|
+
rows="2"
|
|
671
|
+
:placeholder="t('prReview.challenge.placeholder')"
|
|
672
|
+
class="w-full resize-y rounded border border-slate-700 bg-slate-950/60 px-2 py-1 text-[12px] text-slate-200 outline-none focus:border-indigo-500"
|
|
673
|
+
/>
|
|
674
|
+
<p class="mt-1 text-[10px] text-slate-500">
|
|
675
|
+
{{ t('prReview.challenge.hint') }}
|
|
676
|
+
</p>
|
|
677
|
+
<div class="mt-1.5 flex justify-end gap-2">
|
|
678
|
+
<button
|
|
679
|
+
class="rounded px-2 py-1 text-[11px] text-slate-400 hover:text-slate-200"
|
|
680
|
+
@click="cancelChallenge"
|
|
681
|
+
>
|
|
682
|
+
{{ t('common.cancel') }}
|
|
683
|
+
</button>
|
|
684
|
+
<button
|
|
685
|
+
data-testid="pr-review-challenge-submit"
|
|
686
|
+
class="rounded bg-indigo-500/80 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
|
|
687
|
+
:disabled="!canResolve"
|
|
688
|
+
@click="submitChallenge(f.id)"
|
|
689
|
+
>
|
|
690
|
+
{{ t('prReview.challenge.submit') }}
|
|
691
|
+
</button>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
615
694
|
</div>
|
|
616
695
|
</div>
|
|
617
|
-
</
|
|
618
|
-
</
|
|
619
|
-
</
|
|
696
|
+
</article>
|
|
697
|
+
</section>
|
|
698
|
+
</template>
|
|
620
699
|
</template>
|
|
621
|
-
</
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
<!-- Run details: the same timing / model / run id + LLM model-activity rollup (calls,
|
|
703
|
+
tokens) every step-backed result window carries, so the reviewer's run reads the
|
|
704
|
+
same "which run is this / how did the model do" facts as the generic step detail. -->
|
|
705
|
+
<aside
|
|
706
|
+
v-if="step"
|
|
707
|
+
data-testid="pr-review-run-meta"
|
|
708
|
+
class="hidden w-60 shrink-0 flex-col gap-4 overflow-y-auto border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
|
|
709
|
+
>
|
|
710
|
+
<StepRunMeta
|
|
711
|
+
:step="step"
|
|
712
|
+
:instance-id="instanceId ?? undefined"
|
|
713
|
+
:step-number="stepIndex === null ? undefined : stepIndex + 1"
|
|
714
|
+
:total-steps="instance?.steps.length"
|
|
715
|
+
:run-failed="instance?.status === 'failed'"
|
|
716
|
+
:failure-at="instance?.failure?.occurredAt"
|
|
717
|
+
/>
|
|
718
|
+
</aside>
|
|
622
719
|
</div>
|
|
623
720
|
|
|
624
721
|
<!-- Footer -->
|