@cat-factory/app 0.147.1 → 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 +418 -395
- package/app/utils/prReviewProgress.spec.ts +51 -2
- package/app/utils/prReviewProgress.ts +52 -1
- package/i18n/locales/de.json +8 -0
- package/i18n/locales/en.json +8 -0
- package/i18n/locales/es.json +8 -0
- package/i18n/locales/fr.json +8 -0
- package/i18n/locales/he.json +8 -0
- package/i18n/locales/it.json +8 -0
- package/i18n/locales/ja.json +8 -0
- package/i18n/locales/pl.json +8 -0
- package/i18n/locales/tr.json +8 -0
- package/i18n/locales/uk.json +8 -0
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import { subtaskIconClass } from '~/utils/pipelineRender'
|
|
24
24
|
import { activeChunkLabels, chunkReviewPercent, isSlicingChunks } from '~/utils/prReviewProgress'
|
|
25
25
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
26
|
+
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
26
27
|
|
|
27
28
|
const execution = useExecutionStore()
|
|
28
29
|
const board = useBoardStore()
|
|
@@ -253,446 +254,468 @@ async function onDismiss(id: string): Promise<void> {
|
|
|
253
254
|
</a>
|
|
254
255
|
</template>
|
|
255
256
|
|
|
256
|
-
<div class="min-h-0 flex-1
|
|
257
|
-
|
|
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 —
|
|
258
260
|
SLICING (still grouping the diff into chunks, no plan yet) vs REVIEWING (slicing done,
|
|
259
261
|
working through the chunks) — so the copy never claims "reviewing" while it's slicing. -->
|
|
260
|
-
<div
|
|
261
|
-
v-if="status === 'reviewing'"
|
|
262
|
-
data-testid="pr-review-reviewing"
|
|
263
|
-
class="flex h-full flex-col"
|
|
264
|
-
>
|
|
265
|
-
<!-- SLICING: no todo list yet — the reviewer is still grouping the diff into chunks. -->
|
|
266
262
|
<div
|
|
267
|
-
v-if="
|
|
268
|
-
data-testid="pr-review-
|
|
269
|
-
class="flex h-full flex-col
|
|
263
|
+
v-if="status === 'reviewing'"
|
|
264
|
+
data-testid="pr-review-reviewing"
|
|
265
|
+
class="flex h-full flex-col"
|
|
270
266
|
>
|
|
271
|
-
|
|
272
|
-
<p class="text-sm text-slate-200">{{ t('prReview.reviewing.slicing.title') }}</p>
|
|
273
|
-
<p class="max-w-sm text-[11px] text-slate-500">
|
|
274
|
-
{{ t('prReview.reviewing.slicing.hint') }}
|
|
275
|
-
</p>
|
|
276
|
-
</div>
|
|
277
|
-
|
|
278
|
-
<!-- REVIEWING: slicing is done — show every chunk with its status + which are active now. -->
|
|
279
|
-
<div v-else data-testid="pr-review-reviewing-chunks" class="py-2">
|
|
280
|
-
<div class="mb-1 flex items-center gap-2 text-sm text-slate-200">
|
|
281
|
-
<UIcon
|
|
282
|
-
name="i-lucide-loader-circle"
|
|
283
|
-
class="h-4 w-4 shrink-0 animate-spin text-indigo-300"
|
|
284
|
-
/>
|
|
285
|
-
<span>{{ t('prReview.reviewing.reviewingChunks.title') }}</span>
|
|
286
|
-
</div>
|
|
287
|
-
<p class="mb-3 text-[11px] text-slate-500">
|
|
288
|
-
{{ t('prReview.reviewing.reviewingChunks.hint') }}
|
|
289
|
-
</p>
|
|
290
|
-
|
|
291
|
-
<div class="flex items-center justify-between text-[11px] text-slate-400">
|
|
292
|
-
<span data-testid="pr-review-chunk-count">
|
|
293
|
-
{{
|
|
294
|
-
t('prReview.reviewing.chunks', {
|
|
295
|
-
completed: subtasks!.completed,
|
|
296
|
-
total: subtasks!.total,
|
|
297
|
-
})
|
|
298
|
-
}}
|
|
299
|
-
</span>
|
|
300
|
-
<span v-if="subtasks!.inProgress > 0" class="text-indigo-300">
|
|
301
|
-
{{ t('prReview.reviewing.inProgress', { count: subtasks!.inProgress }) }}
|
|
302
|
-
</span>
|
|
303
|
-
</div>
|
|
304
|
-
<div class="mt-1 h-1.5 overflow-hidden rounded-full bg-slate-700/60">
|
|
305
|
-
<div
|
|
306
|
-
class="h-full rounded-full bg-indigo-400 transition-all duration-500"
|
|
307
|
-
:style="{ width: `${chunkPercent}%` }"
|
|
308
|
-
/>
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
|
-
<!-- The chunk(s) being actively reviewed right now, called out on their own. -->
|
|
267
|
+
<!-- SLICING: no todo list yet — the reviewer is still grouping the diff into chunks. -->
|
|
312
268
|
<div
|
|
313
|
-
v-if="
|
|
314
|
-
data-testid="pr-review-
|
|
315
|
-
class="
|
|
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"
|
|
316
272
|
>
|
|
317
|
-
<
|
|
318
|
-
|
|
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') }}
|
|
319
277
|
</p>
|
|
320
|
-
<ul class="space-y-1">
|
|
321
|
-
<li
|
|
322
|
-
v-for="(label, i) in activeChunks"
|
|
323
|
-
:key="i"
|
|
324
|
-
class="flex items-start gap-1.5 text-[12px] text-slate-100"
|
|
325
|
-
>
|
|
326
|
-
<UIcon
|
|
327
|
-
name="i-lucide-loader-circle"
|
|
328
|
-
class="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin text-indigo-300"
|
|
329
|
-
/>
|
|
330
|
-
<span class="min-w-0">{{ label }}</span>
|
|
331
|
-
</li>
|
|
332
|
-
</ul>
|
|
333
278
|
</div>
|
|
334
279
|
|
|
335
|
-
<!--
|
|
336
|
-
<
|
|
337
|
-
<
|
|
338
|
-
|
|
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
|
+
<UIcon
|
|
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') }}
|
|
339
291
|
</p>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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}%` }"
|
|
310
|
+
/>
|
|
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"
|
|
362
327
|
>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
class="
|
|
368
|
-
|
|
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
|
+
"
|
|
369
357
|
>
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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>
|
|
375
380
|
</div>
|
|
376
|
-
</div>
|
|
377
|
-
|
|
378
|
-
<!-- A resolution is executing: the Fixer is committing / comments are being posted. -->
|
|
379
|
-
<div
|
|
380
|
-
v-else-if="working"
|
|
381
|
-
data-testid="pr-review-working"
|
|
382
|
-
class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
383
|
-
>
|
|
384
|
-
<UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
|
|
385
|
-
<p class="text-sm">
|
|
386
|
-
{{ status === 'fixing' ? t('prReview.fixing.title') : t('prReview.posting.title') }}
|
|
387
|
-
</p>
|
|
388
|
-
<p class="max-w-sm text-[11px] text-slate-500">
|
|
389
|
-
{{ status === 'fixing' ? t('prReview.fixing.hint') : t('prReview.posting.hint') }}
|
|
390
|
-
</p>
|
|
391
|
-
</div>
|
|
392
|
-
|
|
393
|
-
<template v-else>
|
|
394
|
-
<p
|
|
395
|
-
v-if="prReview.error"
|
|
396
|
-
class="mb-3 rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
|
|
397
|
-
>
|
|
398
|
-
{{ prReview.error }}
|
|
399
|
-
</p>
|
|
400
381
|
|
|
401
|
-
<!--
|
|
402
|
-
which failed + why, and how many findings were folded into the summary because their
|
|
403
|
-
line isn't in the PR diff. Surfaced so a partial/failed post is legible + retryable. -->
|
|
382
|
+
<!-- A resolution is executing: the Fixer is committing / comments are being posted. -->
|
|
404
383
|
<div
|
|
405
|
-
v-if="
|
|
406
|
-
data-testid="pr-review-
|
|
407
|
-
class="
|
|
408
|
-
:class="
|
|
409
|
-
postReport.failures.length > 0 || postReport.bodyPosted === false
|
|
410
|
-
? 'border-amber-500/40 bg-amber-500/10 text-amber-200'
|
|
411
|
-
: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-200'
|
|
412
|
-
"
|
|
384
|
+
v-else-if="working"
|
|
385
|
+
data-testid="pr-review-working"
|
|
386
|
+
class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
413
387
|
>
|
|
414
|
-
<
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
postReport.failures.length > 0 || postReport.bodyPosted === false
|
|
418
|
-
? 'i-lucide-alert-triangle'
|
|
419
|
-
: 'i-lucide-check-circle-2'
|
|
420
|
-
"
|
|
421
|
-
class="h-4 w-4 shrink-0"
|
|
422
|
-
/>
|
|
423
|
-
<span>{{ t('prReview.postReport.heading') }}</span>
|
|
424
|
-
</div>
|
|
425
|
-
<p data-testid="pr-review-post-count">
|
|
426
|
-
{{
|
|
427
|
-
t('prReview.postReport.posted', {
|
|
428
|
-
posted: postReport.posted,
|
|
429
|
-
attempted: postReport.attempted,
|
|
430
|
-
})
|
|
431
|
-
}}
|
|
432
|
-
</p>
|
|
433
|
-
<p v-if="postReport.folded > 0" class="mt-0.5 opacity-90">
|
|
434
|
-
{{ t('prReview.postReport.folded', { count: postReport.folded }) }}
|
|
388
|
+
<UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
|
|
389
|
+
<p class="text-sm">
|
|
390
|
+
{{ status === 'fixing' ? t('prReview.fixing.title') : t('prReview.posting.title') }}
|
|
435
391
|
</p>
|
|
436
|
-
<
|
|
437
|
-
|
|
438
|
-
<ul class="mt-0.5 space-y-0.5" data-testid="pr-review-post-failures">
|
|
439
|
-
<li v-for="f in postReport.failures" :key="f.findingId" class="flex gap-1.5">
|
|
440
|
-
<code class="shrink-0 text-amber-100"
|
|
441
|
-
>{{ f.path }}<template v-if="f.line != null">:{{ f.line }}</template></code
|
|
442
|
-
>
|
|
443
|
-
<span class="opacity-90">— {{ f.reason }}</span>
|
|
444
|
-
</li>
|
|
445
|
-
</ul>
|
|
446
|
-
</template>
|
|
447
|
-
<p v-if="postReport.bodyPosted === false && postReport.bodyError" class="mt-1.5">
|
|
448
|
-
{{ t('prReview.postReport.bodyError', { error: postReport.bodyError }) }}
|
|
392
|
+
<p class="max-w-sm text-[11px] text-slate-500">
|
|
393
|
+
{{ status === 'fixing' ? t('prReview.fixing.hint') : t('prReview.posting.hint') }}
|
|
449
394
|
</p>
|
|
450
395
|
</div>
|
|
451
396
|
|
|
452
|
-
<!-- The reviewer's overall assessment. -->
|
|
453
|
-
<p
|
|
454
|
-
v-if="state?.summary"
|
|
455
|
-
class="mb-3 rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
|
|
456
|
-
>
|
|
457
|
-
<span class="text-slate-500">{{ t('prReview.summaryLabel') }}</span>
|
|
458
|
-
{{ state.summary }}
|
|
459
|
-
</p>
|
|
460
|
-
|
|
461
|
-
<!-- A clean PR / resolved review with no findings. -->
|
|
462
|
-
<div
|
|
463
|
-
v-if="findings.length === 0"
|
|
464
|
-
class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-6 text-center text-[13px] text-slate-300"
|
|
465
|
-
>
|
|
466
|
-
{{ t('prReview.noFindings') }}
|
|
467
|
-
</div>
|
|
468
|
-
|
|
469
397
|
<template v-else>
|
|
470
|
-
|
|
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>
|
|
404
|
+
|
|
405
|
+
<!-- The outcome of the most recent `post` attempt: how many of how many comments landed,
|
|
406
|
+
which failed + why, and how many findings were folded into the summary because their
|
|
407
|
+
line isn't in the PR diff. Surfaced so a partial/failed post is legible + retryable. -->
|
|
471
408
|
<div
|
|
472
|
-
v-if="
|
|
473
|
-
data-testid="pr-review-
|
|
474
|
-
class="mb-3
|
|
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
|
+
"
|
|
475
417
|
>
|
|
476
|
-
<
|
|
477
|
-
|
|
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>
|
|
478
454
|
</div>
|
|
479
455
|
|
|
480
|
-
<!--
|
|
481
|
-
<
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
<
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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>
|
|
464
|
+
|
|
465
|
+
<!-- A clean PR / resolved review with no findings. -->
|
|
466
|
+
<div
|
|
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"
|
|
469
|
+
>
|
|
470
|
+
{{ t('prReview.noFindings') }}
|
|
491
471
|
</div>
|
|
492
472
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
<
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
{{ g.rationale }}
|
|
500
|
-
</p>
|
|
501
|
-
<article
|
|
502
|
-
v-for="f in g.items"
|
|
503
|
-
:key="f.id"
|
|
504
|
-
data-testid="pr-review-finding"
|
|
505
|
-
class="mb-1.5 rounded-xl border px-3 py-2 transition"
|
|
506
|
-
:class="[
|
|
507
|
-
awaiting && selected.has(f.id) && !isRetracted(f)
|
|
508
|
-
? 'border-indigo-500/60 bg-indigo-500/5'
|
|
509
|
-
: 'border-slate-800 bg-slate-900/60',
|
|
510
|
-
isRetracted(f) ? 'opacity-60' : '',
|
|
511
|
-
]"
|
|
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"
|
|
512
479
|
>
|
|
513
|
-
<
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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"
|
|
580
597
|
:class="isRetracted(f) ? 'line-through' : ''"
|
|
581
598
|
>
|
|
582
|
-
{{ f.
|
|
583
|
-
</
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}}<template v-if="f.line != null">
|
|
588
|
-
· {{ 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"
|
|
589
604
|
>
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
:class="isRetracted(f) ? 'line-through' : ''"
|
|
594
|
-
>
|
|
595
|
-
{{ f.detail }}
|
|
596
|
-
</p>
|
|
597
|
-
<p
|
|
598
|
-
v-if="f.suggestedFix"
|
|
599
|
-
class="mt-1 whitespace-pre-wrap rounded-md bg-slate-800/50 px-2 py-1 text-[11px] text-slate-300"
|
|
600
|
-
>
|
|
601
|
-
<span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
|
|
602
|
-
{{ f.suggestedFix }}
|
|
603
|
-
</p>
|
|
605
|
+
<span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
|
|
606
|
+
{{ f.suggestedFix }}
|
|
607
|
+
</p>
|
|
604
608
|
|
|
605
|
-
|
|
609
|
+
<!-- The investigator's justification (why the finding holds up / was retracted),
|
|
606
610
|
or the reason the challenge investigation failed. -->
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
>
|
|
619
|
-
<span class="font-medium">{{
|
|
620
|
-
isChallengeFailed(f)
|
|
621
|
-
? t('prReview.challenge.failedLabel')
|
|
622
|
-
: t('prReview.challenge.verdictLabel')
|
|
623
|
-
}}</span>
|
|
624
|
-
{{ f.challenge.justification }}
|
|
625
|
-
</p>
|
|
626
|
-
|
|
627
|
-
<!-- Per-finding actions: Challenge + Dismiss (only while awaiting a selection). -->
|
|
628
|
-
<div
|
|
629
|
-
v-if="awaiting && !isInvestigating(f)"
|
|
630
|
-
class="mt-1.5 flex items-center gap-3 text-[11px]"
|
|
631
|
-
>
|
|
632
|
-
<button
|
|
633
|
-
v-if="!isRetracted(f)"
|
|
634
|
-
data-testid="pr-review-finding-challenge"
|
|
635
|
-
class="flex items-center gap-1 text-indigo-300 hover:underline disabled:opacity-50"
|
|
636
|
-
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
637
|
-
@click="openChallenge(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
|
+
"
|
|
638
622
|
>
|
|
639
|
-
<
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}}
|
|
645
|
-
</button>
|
|
646
|
-
<button
|
|
647
|
-
data-testid="pr-review-finding-dismiss"
|
|
648
|
-
class="flex items-center gap-1 text-slate-400 hover:text-rose-300 hover:underline disabled:opacity-50"
|
|
649
|
-
:disabled="!canResolve || !access.canExecuteRuns.value"
|
|
650
|
-
@click="onDismiss(f.id)"
|
|
651
|
-
>
|
|
652
|
-
<UIcon name="i-lucide-trash-2" class="h-3.5 w-3.5" />
|
|
653
|
-
{{ t('prReview.challenge.dismiss') }}
|
|
654
|
-
</button>
|
|
655
|
-
</div>
|
|
656
|
-
|
|
657
|
-
<!-- The inline challenge box: an OPTIONAL specific concern for the investigator. -->
|
|
658
|
-
<div
|
|
659
|
-
v-if="challengeForId === f.id"
|
|
660
|
-
data-testid="pr-review-challenge-box"
|
|
661
|
-
class="mt-2 rounded-md border border-indigo-500/40 bg-slate-900/80 p-2"
|
|
662
|
-
>
|
|
663
|
-
<textarea
|
|
664
|
-
v-model="challengeText"
|
|
665
|
-
data-testid="pr-review-challenge-input"
|
|
666
|
-
rows="2"
|
|
667
|
-
:placeholder="t('prReview.challenge.placeholder')"
|
|
668
|
-
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"
|
|
669
|
-
/>
|
|
670
|
-
<p class="mt-1 text-[10px] text-slate-500">
|
|
671
|
-
{{ 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 }}
|
|
672
629
|
</p>
|
|
673
|
-
|
|
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
|
+
>
|
|
674
636
|
<button
|
|
675
|
-
|
|
676
|
-
|
|
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)"
|
|
677
642
|
>
|
|
678
|
-
|
|
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
|
+
}}
|
|
679
649
|
</button>
|
|
680
650
|
<button
|
|
681
|
-
data-testid="pr-review-
|
|
682
|
-
class="
|
|
683
|
-
:disabled="!canResolve"
|
|
684
|
-
@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)"
|
|
685
655
|
>
|
|
686
|
-
|
|
656
|
+
<UIcon name="i-lucide-trash-2" class="h-3.5 w-3.5" />
|
|
657
|
+
{{ t('prReview.challenge.dismiss') }}
|
|
687
658
|
</button>
|
|
688
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>
|
|
689
694
|
</div>
|
|
690
695
|
</div>
|
|
691
|
-
</
|
|
692
|
-
</
|
|
693
|
-
</
|
|
696
|
+
</article>
|
|
697
|
+
</section>
|
|
698
|
+
</template>
|
|
694
699
|
</template>
|
|
695
|
-
</
|
|
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>
|
|
696
719
|
</div>
|
|
697
720
|
|
|
698
721
|
<!-- Footer -->
|