@cat-factory/app 0.146.3 → 0.147.1

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.
@@ -17,9 +17,11 @@ 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'
24
26
 
25
27
  const execution = useExecutionStore()
@@ -46,18 +48,51 @@ const step = computed(() => {
46
48
  const state = computed<PrReviewStepState | null>(() => step.value?.prReview ?? null)
47
49
  const status = computed(() => state.value?.status ?? null)
48
50
  const awaiting = computed(() => status.value === 'awaiting_selection')
51
+ // A finding is being re-examined by the Challenge Investigator (the whole review is `challenging`
52
+ // while it runs). The findings stay visible — the challenged one shows a spinner — but the
53
+ // selection controls + per-finding actions are disabled until the verdict lands and it re-parks.
54
+ const challenging = computed(() => status.value === 'challenging')
49
55
  // The reviewer's live todo list while it works, streamed onto the step. Its entries are the
50
- // cohesive slices/chunks the agent grouped the diff into (plus a final "aggregate" step), so it
51
- // surfaces slices-reviewed-so-far progress during the `reviewing` phase richer than a spinner.
56
+ // cohesive slices/chunks the agent grouped the diff into (plus a final "aggregate" step). The
57
+ // derivation of the two `reviewing`-phase sub-states from it lives in `~/utils/prReviewProgress`
58
+ // (pure + unit-tested); see that module for the slicing-vs-reviewing signal.
52
59
  const subtasks = computed<StepSubtasks | null>(() => step.value?.subtasks ?? null)
53
- const hasProgress = computed(() => (subtasks.value?.total ?? 0) > 0)
60
+
61
+ /**
62
+ * Slicing sub-phase: the reviewer is still grouping the diff into chunks (no todo list yet).
63
+ * Once the list exists, slicing is done and we render the per-chunk status list instead.
64
+ */
65
+ const slicing = computed(() => status.value === 'reviewing' && isSlicingChunks(subtasks.value))
66
+
67
+ /** Chunk-review completion, clamped 0..100 for the progress bar. */
68
+ const chunkPercent = computed(() => chunkReviewPercent(subtasks.value))
69
+
70
+ /** The chunks the reviewer is actively working through right now (their labels), for the callout. */
71
+ const activeChunks = computed<string[]>(() => activeChunkLabels(subtasks.value))
54
72
 
55
73
  /** Icon per todo-item status (matches the pipeline timeline's live subtask breakdown). */
56
- const ITEM_ICON: Record<string, string> = {
74
+ const ITEM_ICON: Record<StepSubtaskItem['status'], string> = {
57
75
  completed: 'i-lucide-check-circle-2',
58
76
  in_progress: 'i-lucide-loader-circle',
59
77
  pending: 'i-lucide-circle',
60
78
  }
79
+
80
+ // Per-chunk status label + chip styling. The key map is an exhaustive Record over the subtask
81
+ // status union, so adding a status without a label fails the typecheck (the sanctioned dynamic
82
+ // enum→key pattern — tier 1 can't see a runtime-built key).
83
+ const CHUNK_STATUS_KEY: Record<StepSubtaskItem['status'], string> = {
84
+ completed: 'prReview.reviewing.chunkStatus.completed',
85
+ in_progress: 'prReview.reviewing.chunkStatus.in_progress',
86
+ pending: 'prReview.reviewing.chunkStatus.pending',
87
+ }
88
+ const CHUNK_STATUS_CLASS: Record<StepSubtaskItem['status'], string> = {
89
+ completed: 'bg-emerald-500/15 text-emerald-300',
90
+ in_progress: 'bg-indigo-500/15 text-indigo-300',
91
+ pending: 'bg-slate-700/60 text-slate-400',
92
+ }
93
+ function chunkStatusLabel(status: StepSubtaskItem['status']): string {
94
+ return t(CHUNK_STATUS_KEY[status])
95
+ }
61
96
  // A resolution is executing (the Fixer is committing, or comments are being posted) — show a
62
97
  // working state between the human's choice and the run advancing/the stream echoing `done`.
63
98
  const working = computed(() => status.value === 'fixing' || status.value === 'posting')
@@ -101,6 +136,27 @@ const groups = computed(() => {
101
136
  return out
102
137
  })
103
138
 
139
+ /** A finding the Challenge Investigator RETRACTED — it can no longer be acted on (auto-deselected). */
140
+ function isRetracted(f: PrReviewFinding): boolean {
141
+ return f.challenge?.status === 'retracted'
142
+ }
143
+ /** A finding currently being re-examined by the Challenge Investigator. */
144
+ function isInvestigating(f: PrReviewFinding): boolean {
145
+ return f.challenge?.status === 'investigating'
146
+ }
147
+ /** A finding the investigator UPHELD + strengthened after a challenge (its body actually changed). */
148
+ function isAmended(f: PrReviewFinding): boolean {
149
+ return f.challenge?.status === 'amended'
150
+ }
151
+ /** A finding the investigator UPHELD as written after a challenge (kept, no revision). */
152
+ function isUpheld(f: PrReviewFinding): boolean {
153
+ return f.challenge?.status === 'upheld'
154
+ }
155
+ /** A finding whose challenge investigation FAILED — the finding is kept as-is; re-challenge allowed. */
156
+ function isChallengeFailed(f: PrReviewFinding): boolean {
157
+ return f.challenge?.status === 'failed'
158
+ }
159
+
104
160
  // The human's selection — a set of finding ids. Defaults to every finding (the human deselects
105
161
  // the noise), so "Finish" without touching anything keeps all. Re-seeded ONLY when the set of
106
162
  // finding IDS actually changes — NOT on every execution-stream re-emit (which hands us a fresh
@@ -111,11 +167,17 @@ const selected = ref<Set<string>>(new Set())
111
167
  watch(
112
168
  findingIdKey,
113
169
  () => {
114
- selected.value = new Set(findings.value.map((f) => f.id))
170
+ selected.value = new Set(findings.value.filter((f) => !isRetracted(f)).map((f) => f.id))
115
171
  },
116
172
  { immediate: true },
117
173
  )
118
174
 
175
+ // The effective selection: checked AND not retracted. A retracted finding is never acted on even
176
+ // if its box was checked before the investigator dropped it (it's disabled + unchecked visually).
177
+ const activeSelectedIds = computed(() =>
178
+ findings.value.filter((f) => selected.value.has(f.id) && !isRetracted(f)).map((f) => f.id),
179
+ )
180
+
119
181
  function toggle(id: string): void {
120
182
  const next = new Set(selected.value)
121
183
  if (next.has(id)) next.delete(id)
@@ -123,7 +185,7 @@ function toggle(id: string): void {
123
185
  selected.value = next
124
186
  }
125
187
  function selectAll(): void {
126
- selected.value = new Set(findings.value.map((f) => f.id))
188
+ selected.value = new Set(findings.value.filter((f) => !isRetracted(f)).map((f) => f.id))
127
189
  }
128
190
  function clearAll(): void {
129
191
  selected.value = new Set()
@@ -132,13 +194,40 @@ function clearAll(): void {
132
194
  const canResolve = computed(() => awaiting.value && !prReview.resolving)
133
195
  // Fix / Post act on the selection, so they need at least one selected finding; Finish always
134
196
  // works (it just records the — possibly empty — curated selection and completes the review).
135
- const hasSelection = computed(() => selected.value.size > 0)
197
+ const hasSelection = computed(() => activeSelectedIds.value.length > 0)
136
198
 
137
199
  async function onResolve(action: PrReviewResolution): Promise<void> {
138
200
  const id = instanceId.value
139
201
  if (!id || !canResolve.value) return
140
202
  if ((action === 'fix' || action === 'post') && !hasSelection.value) return
141
- await prReview.resolve(id, [...selected.value], action).catch(() => {})
203
+ await prReview.resolve(id, activeSelectedIds.value, action).catch(() => {})
204
+ }
205
+
206
+ // Per-finding CHALLENGE: the open finding's id (its inline concern box is showing) + the drafted
207
+ // concern text. Dispatching moves the whole review to `challenging` until the verdict lands.
208
+ const challengeForId = ref<string | null>(null)
209
+ const challengeText = ref('')
210
+ function openChallenge(id: string): void {
211
+ challengeForId.value = id
212
+ challengeText.value = ''
213
+ }
214
+ function cancelChallenge(): void {
215
+ challengeForId.value = null
216
+ challengeText.value = ''
217
+ }
218
+ async function submitChallenge(id: string): Promise<void> {
219
+ const inst = instanceId.value
220
+ if (!inst || !canResolve.value) return
221
+ const question = challengeText.value.trim()
222
+ challengeForId.value = null
223
+ challengeText.value = ''
224
+ await prReview.challenge(inst, id, question || undefined).catch(() => {})
225
+ }
226
+ async function onDismiss(id: string): Promise<void> {
227
+ const inst = instanceId.value
228
+ if (!inst || !canResolve.value) return
229
+ if (challengeForId.value === id) cancelChallenge()
230
+ await prReview.dismiss(inst, id).catch(() => {})
142
231
  }
143
232
  </script>
144
233
 
@@ -165,24 +254,39 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
165
254
  </template>
166
255
 
167
256
  <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
168
- <!-- Reviewing: the read-only reviewer is still working. Once it starts maintaining its
169
- per-slice todo list, surface the live chunk progress (slices reviewed / total + the
170
- breakdown) instead of a bare spinner. -->
257
+ <!-- Reviewing: the read-only reviewer is still working. The phase is told apart precisely —
258
+ SLICING (still grouping the diff into chunks, no plan yet) vs REVIEWING (slicing done,
259
+ working through the chunks) so the copy never claims "reviewing" while it's slicing. -->
171
260
  <div
172
261
  v-if="status === 'reviewing'"
173
262
  data-testid="pr-review-reviewing"
174
263
  class="flex h-full flex-col"
175
264
  >
176
- <!-- Live chunk progress once the reviewer has planned its slices. -->
177
- <div v-if="hasProgress" class="py-2">
265
+ <!-- SLICING: no todo list yet — the reviewer is still grouping the diff into chunks. -->
266
+ <div
267
+ v-if="slicing"
268
+ data-testid="pr-review-slicing"
269
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
270
+ >
271
+ <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
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">
178
280
  <div class="mb-1 flex items-center gap-2 text-sm text-slate-200">
179
281
  <UIcon
180
282
  name="i-lucide-loader-circle"
181
283
  class="h-4 w-4 shrink-0 animate-spin text-indigo-300"
182
284
  />
183
- <span>{{ t('prReview.reviewing.title') }}</span>
285
+ <span>{{ t('prReview.reviewing.reviewingChunks.title') }}</span>
184
286
  </div>
185
- <p class="mb-3 text-[11px] text-slate-500">{{ t('prReview.reviewing.hint') }}</p>
287
+ <p class="mb-3 text-[11px] text-slate-500">
288
+ {{ t('prReview.reviewing.reviewingChunks.hint') }}
289
+ </p>
186
290
 
187
291
  <div class="flex items-center justify-between text-[11px] text-slate-400">
188
292
  <span data-testid="pr-review-chunk-count">
@@ -200,46 +304,74 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
200
304
  <div class="mt-1 h-1.5 overflow-hidden rounded-full bg-slate-700/60">
201
305
  <div
202
306
  class="h-full rounded-full bg-indigo-400 transition-all duration-500"
203
- :style="{ width: `${(subtasks!.completed / subtasks!.total) * 100}%` }"
307
+ :style="{ width: `${chunkPercent}%` }"
204
308
  />
205
309
  </div>
206
310
 
207
- <!-- The slice/todo breakdown the agent is working through. -->
208
- <ul
209
- v-if="subtasks!.items?.length"
210
- class="mt-3 space-y-1.5"
211
- data-testid="pr-review-chunks"
311
+ <!-- The chunk(s) being actively reviewed right now, called out on their own. -->
312
+ <div
313
+ v-if="activeChunks.length"
314
+ data-testid="pr-review-active-chunks"
315
+ class="mt-3 rounded-lg border border-indigo-500/30 bg-indigo-500/5 px-2.5 py-2"
212
316
  >
213
- <li
214
- v-for="(item, i) in subtasks!.items"
215
- :key="i"
216
- class="flex items-start gap-1.5 text-[12px]"
217
- :class="
218
- item.status === 'completed'
219
- ? 'text-slate-500 line-through'
220
- : item.status === 'in_progress'
221
- ? 'text-slate-100'
222
- : 'text-slate-400'
223
- "
224
- >
225
- <UIcon
226
- :name="ITEM_ICON[item.status]"
227
- class="mt-0.5 h-3.5 w-3.5 shrink-0"
228
- :class="subtaskIconClass(item.status, false)"
229
- />
230
- <span>{{ item.label }}</span>
231
- </li>
232
- </ul>
233
- </div>
317
+ <p class="mb-1 text-[10px] font-semibold uppercase tracking-wide text-indigo-300">
318
+ {{ t('prReview.reviewing.activeHeading') }}
319
+ </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
+ </div>
234
334
 
235
- <!-- Before the reviewer has planned its slices: the cold-start spinner. -->
236
- <div
237
- v-else
238
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
239
- >
240
- <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
241
- <p class="text-sm">{{ t('prReview.reviewing.title') }}</p>
242
- <p class="max-w-sm text-[11px] text-slate-500">{{ t('prReview.reviewing.hint') }}</p>
335
+ <!-- Every chunk with its explicit status (Reviewed / Reviewing… / Queued). -->
336
+ <template v-if="subtasks!.items?.length">
337
+ <p class="mb-1.5 mt-3 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
338
+ {{ t('prReview.reviewing.chunksHeading') }}
339
+ </p>
340
+ <ul class="space-y-1.5" data-testid="pr-review-chunks">
341
+ <li
342
+ v-for="(item, i) in subtasks!.items"
343
+ :key="i"
344
+ data-testid="pr-review-chunk"
345
+ class="flex items-center gap-1.5 text-[12px]"
346
+ :class="
347
+ item.status === 'completed'
348
+ ? 'text-slate-500'
349
+ : item.status === 'in_progress'
350
+ ? 'text-slate-100'
351
+ : 'text-slate-400'
352
+ "
353
+ >
354
+ <UIcon
355
+ :name="ITEM_ICON[item.status]"
356
+ class="h-3.5 w-3.5 shrink-0"
357
+ :class="subtaskIconClass(item.status, false)"
358
+ />
359
+ <span
360
+ class="min-w-0 flex-1 truncate"
361
+ :class="item.status === 'completed' ? 'line-through' : ''"
362
+ >
363
+ {{ item.label }}
364
+ </span>
365
+ <span
366
+ data-testid="pr-review-chunk-status"
367
+ class="shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium uppercase"
368
+ :class="CHUNK_STATUS_CLASS[item.status]"
369
+ >
370
+ {{ chunkStatusLabel(item.status) }}
371
+ </span>
372
+ </li>
373
+ </ul>
374
+ </template>
243
375
  </div>
244
376
  </div>
245
377
 
@@ -335,10 +467,20 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
335
467
  </div>
336
468
 
337
469
  <template v-else>
470
+ <!-- A challenge is in flight: the Challenge Investigator is re-examining a finding. -->
471
+ <div
472
+ v-if="challenging"
473
+ data-testid="pr-review-challenging"
474
+ 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"
475
+ >
476
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 shrink-0 animate-spin" />
477
+ <span>{{ t('prReview.challenge.investigatingBanner') }}</span>
478
+ </div>
479
+
338
480
  <!-- Selection toolbar -->
339
481
  <div v-if="awaiting" class="mb-2 flex items-center gap-3 text-[11px] text-slate-400">
340
482
  <span data-testid="pr-review-selected-count">
341
- {{ t('prReview.selectedCount', { count: selected.size }) }}
483
+ {{ t('prReview.selectedCount', { count: activeSelectedIds.length }) }}
342
484
  </span>
343
485
  <button class="text-indigo-300 hover:underline" @click="selectAll">
344
486
  {{ t('prReview.selectAll') }}
@@ -361,19 +503,21 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
361
503
  :key="f.id"
362
504
  data-testid="pr-review-finding"
363
505
  class="mb-1.5 rounded-xl border px-3 py-2 transition"
364
- :class="
365
- awaiting && selected.has(f.id)
506
+ :class="[
507
+ awaiting && selected.has(f.id) && !isRetracted(f)
366
508
  ? 'border-indigo-500/60 bg-indigo-500/5'
367
- : 'border-slate-800 bg-slate-900/60'
368
- "
509
+ : 'border-slate-800 bg-slate-900/60',
510
+ isRetracted(f) ? 'opacity-60' : '',
511
+ ]"
369
512
  >
370
513
  <div class="flex items-start gap-2">
371
514
  <input
372
- v-if="awaiting"
515
+ v-if="awaiting || challenging"
373
516
  type="checkbox"
374
517
  class="mt-1 accent-indigo-500"
375
518
  data-testid="pr-review-finding-toggle"
376
- :checked="selected.has(f.id)"
519
+ :checked="selected.has(f.id) && !isRetracted(f)"
520
+ :disabled="!awaiting || isRetracted(f)"
377
521
  @change="toggle(f.id)"
378
522
  />
379
523
  <div class="min-w-0 flex-1">
@@ -394,7 +538,47 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
394
538
  >
395
539
  {{ t('prReview.postReport.postedBadge') }}
396
540
  </span>
397
- <h4 class="min-w-0 flex-1 text-[13px] font-medium text-slate-100">
541
+ <!-- Challenge outcome badges -->
542
+ <span
543
+ v-if="isRetracted(f)"
544
+ data-testid="pr-review-finding-retracted"
545
+ 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"
546
+ >
547
+ {{ t('prReview.challenge.retractedBadge') }}
548
+ </span>
549
+ <span
550
+ v-else-if="isAmended(f)"
551
+ data-testid="pr-review-finding-amended"
552
+ 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"
553
+ >
554
+ {{ t('prReview.challenge.strengthenedBadge') }}
555
+ </span>
556
+ <span
557
+ v-else-if="isUpheld(f)"
558
+ data-testid="pr-review-finding-upheld"
559
+ 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"
560
+ >
561
+ {{ t('prReview.challenge.upheldBadge') }}
562
+ </span>
563
+ <span
564
+ v-else-if="isChallengeFailed(f)"
565
+ data-testid="pr-review-finding-challenge-failed"
566
+ 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"
567
+ >
568
+ {{ t('prReview.challenge.failedBadge') }}
569
+ </span>
570
+ <span
571
+ v-else-if="isInvestigating(f)"
572
+ data-testid="pr-review-finding-investigating"
573
+ 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"
574
+ >
575
+ <UIcon name="i-lucide-loader-circle" class="h-3 w-3 animate-spin" />
576
+ {{ t('prReview.challenge.investigatingBadge') }}
577
+ </span>
578
+ <h4
579
+ class="min-w-0 flex-1 text-[13px] font-medium text-slate-100"
580
+ :class="isRetracted(f) ? 'line-through' : ''"
581
+ >
398
582
  {{ f.title }}
399
583
  </h4>
400
584
  </div>
@@ -404,7 +588,10 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
404
588
  · {{ t('prReview.line', { line: f.line }) }}</template
405
589
  >
406
590
  </p>
407
- <p class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
591
+ <p
592
+ class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300"
593
+ :class="isRetracted(f) ? 'line-through' : ''"
594
+ >
408
595
  {{ f.detail }}
409
596
  </p>
410
597
  <p
@@ -414,6 +601,92 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
414
601
  <span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
415
602
  {{ f.suggestedFix }}
416
603
  </p>
604
+
605
+ <!-- The investigator's justification (why the finding holds up / was retracted),
606
+ or the reason the challenge investigation failed. -->
607
+ <p
608
+ v-if="f.challenge?.justification"
609
+ data-testid="pr-review-finding-justification"
610
+ class="mt-1.5 whitespace-pre-wrap rounded-md px-2 py-1 text-[11px]"
611
+ :class="
612
+ isRetracted(f)
613
+ ? 'bg-rose-500/10 text-rose-200'
614
+ : isChallengeFailed(f)
615
+ ? 'bg-amber-500/10 text-amber-200'
616
+ : 'bg-sky-500/10 text-sky-200'
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)"
638
+ >
639
+ <UIcon name="i-lucide-gavel" class="h-3.5 w-3.5" />
640
+ {{
641
+ f.challenge
642
+ ? t('prReview.challenge.reChallenge')
643
+ : t('prReview.challenge.action')
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') }}
672
+ </p>
673
+ <div class="mt-1.5 flex justify-end gap-2">
674
+ <button
675
+ class="rounded px-2 py-1 text-[11px] text-slate-400 hover:text-slate-200"
676
+ @click="cancelChallenge"
677
+ >
678
+ {{ t('common.cancel') }}
679
+ </button>
680
+ <button
681
+ data-testid="pr-review-challenge-submit"
682
+ class="rounded bg-indigo-500/80 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
683
+ :disabled="!canResolve"
684
+ @click="submitChallenge(f.id)"
685
+ >
686
+ {{ t('prReview.challenge.submit') }}
687
+ </button>
688
+ </div>
689
+ </div>
417
690
  </div>
418
691
  </div>
419
692
  </article>
@@ -1,4 +1,9 @@
1
- import { getPrReviewContract, resolvePrReviewContract } from '@cat-factory/contracts'
1
+ import {
2
+ challengePrReviewFindingContract,
3
+ dismissPrReviewFindingContract,
4
+ getPrReviewContract,
5
+ resolvePrReviewContract,
6
+ } from '@cat-factory/contracts'
2
7
  import type { ApiContext } from './context'
3
8
 
4
9
  /**
@@ -26,5 +31,25 @@ export function prReviewApi({ send, ws }: ApiContext) {
26
31
  pathParams: { executionId },
27
32
  body,
28
33
  }),
34
+
35
+ // Dismiss a parked finding entirely (drops it + prunes it from the selection).
36
+ dismissPrReviewFinding: (workspaceId: string, executionId: string, findingId: string) =>
37
+ send(dismissPrReviewFindingContract, {
38
+ pathPrefix: ws(workspaceId),
39
+ pathParams: { executionId, findingId },
40
+ }),
41
+
42
+ // Challenge a parked finding — dispatch the Challenge Investigator (optional specific concern).
43
+ challengePrReviewFinding: (
44
+ workspaceId: string,
45
+ executionId: string,
46
+ findingId: string,
47
+ body: { question?: string },
48
+ ) =>
49
+ send(challengePrReviewFindingContract, {
50
+ pathPrefix: ws(workspaceId),
51
+ pathParams: { executionId, findingId },
52
+ body,
53
+ }),
29
54
  }
30
55
  }
@@ -82,5 +82,50 @@ export const usePrReviewStore = defineStore('prReview', () => {
82
82
  }
83
83
  }
84
84
 
85
- return { resolving, error, load, resolve }
85
+ /** Dismiss a finding entirely: it's removed from the review (and the selection). Stays parked. */
86
+ async function dismiss(executionId: string, findingId: string): Promise<void> {
87
+ error.value = null
88
+ resolving.value = true
89
+ try {
90
+ const state = await api.dismissPrReviewFinding(workspace.requireId(), executionId, findingId)
91
+ reflect(executionId, state as PrReviewStepState)
92
+ } catch (e) {
93
+ error.value = e instanceof Error ? e.message : 'Failed to dismiss finding'
94
+ throw e
95
+ } finally {
96
+ resolving.value = false
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Challenge a finding: dispatch the Challenge Investigator with an OPTIONAL specific concern
102
+ * (blank ⇒ the generic "dig deeper + validate" prompt). The review moves to `challenging` while
103
+ * the investigator digs in; its verdict (strengthen / retract the finding) arrives via the stream.
104
+ */
105
+ async function challenge(
106
+ executionId: string,
107
+ findingId: string,
108
+ question?: string,
109
+ ): Promise<void> {
110
+ error.value = null
111
+ resolving.value = true
112
+ try {
113
+ const state = await api.challengePrReviewFinding(
114
+ workspace.requireId(),
115
+ executionId,
116
+ findingId,
117
+ {
118
+ question,
119
+ },
120
+ )
121
+ reflect(executionId, state as PrReviewStepState)
122
+ } catch (e) {
123
+ error.value = e instanceof Error ? e.message : 'Failed to challenge finding'
124
+ throw e
125
+ } finally {
126
+ resolving.value = false
127
+ }
128
+ }
129
+
130
+ return { resolving, error, load, resolve, dismiss, challenge }
86
131
  })
@@ -45,6 +45,7 @@ export type {
45
45
  ForkDecisionStepState,
46
46
  PrReviewStepState,
47
47
  PrReviewFinding,
48
+ PrReviewFindingChallenge,
48
49
  PrReviewSlice,
49
50
  PrReviewSeverity,
50
51
  PrReviewCategory,
@@ -412,6 +412,20 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
412
412
  color: '#22d3ee',
413
413
  description: 'Maps the repository into the service → modules blueprint.',
414
414
  },
415
+ // The read-only Challenge Investigator: dispatched off a parked `pr-reviewer` step when a human
416
+ // challenges a finding, it re-examines that ONE finding against the full source and upholds
417
+ // (strengthening) or retracts it. Never a palette block; modelled here purely so it is a
418
+ // configurable per-kind default in the Model Defaults panel — a workspace can point it at a
419
+ // different (stronger) model than the reviewer. Its output renders in the pr-review window.
420
+ 'challenge-investigator': {
421
+ kind: 'challenge-investigator',
422
+ label: 'Challenge Investigator',
423
+ icon: 'i-lucide-gavel',
424
+ color: '#6366f1',
425
+ description:
426
+ 'Re-examines a single challenged PR-review finding against the full source, then upholds ' +
427
+ '(strengthening it) or retracts it with a justification. Configurable separately from the reviewer.',
428
+ },
415
429
  // The single environment provisioner: an operational (non-LLM) step that stands up the ephemeral
416
430
  // environment the tester / human-test gate run against for a kubernetes/custom service, and is a
417
431
  // fast no-op for docker-compose / infraless. Seeded before the first tester/human-test step in the
@@ -634,6 +648,9 @@ export const MODEL_CONFIGURABLE_SYSTEM_KINDS: AgentArchetype[] = [
634
648
  'fixer',
635
649
  'merger',
636
650
  'kaizen',
651
+ // The PR-review Challenge Investigator — pinnable to its own (stronger) model, separately
652
+ // from the reviewer that produced the findings.
653
+ 'challenge-investigator',
637
654
  ].map((kind) => SYSTEM_AGENT_META[kind]!),
638
655
  // Companions run LLMs but aren't palette-addable (they're producer toggles), so include
639
656
  // them here to keep their per-workspace default model pinnable in the Model Defaults panel.
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { StepSubtasks } from '~/types/execution'
3
+ import { activeChunkLabels, chunkReviewPercent, isSlicingChunks } from './prReviewProgress'
4
+
5
+ const subtasks = (over: Partial<StepSubtasks>): StepSubtasks => ({
6
+ completed: 0,
7
+ inProgress: 0,
8
+ total: 0,
9
+ ...over,
10
+ })
11
+
12
+ describe('isSlicingChunks', () => {
13
+ it('is slicing when there is no todo list yet (null/undefined or empty)', () => {
14
+ // No plan committed → the reviewer is still grouping the diff into chunks.
15
+ expect(isSlicingChunks(null)).toBe(true)
16
+ expect(isSlicingChunks(undefined)).toBe(true)
17
+ expect(isSlicingChunks(subtasks({ total: 0 }))).toBe(true)
18
+ })
19
+
20
+ it('is not slicing once the todo list exists (slicing is done)', () => {
21
+ expect(isSlicingChunks(subtasks({ total: 3, completed: 1 }))).toBe(false)
22
+ // A single-chunk plan still counts as sliced — a plan of size 1 is a committed plan.
23
+ expect(isSlicingChunks(subtasks({ total: 1 }))).toBe(false)
24
+ })
25
+ })
26
+
27
+ describe('chunkReviewPercent', () => {
28
+ it('is 0 with no plan (avoids 0/0 → NaN)', () => {
29
+ expect(chunkReviewPercent(null)).toBe(0)
30
+ expect(chunkReviewPercent(subtasks({ total: 0, completed: 0 }))).toBe(0)
31
+ })
32
+
33
+ it('rounds completion to an integer percent', () => {
34
+ expect(chunkReviewPercent(subtasks({ total: 4, completed: 1 }))).toBe(25)
35
+ // 1/3 → 33.33… rounds to 33 (the old inline math emitted a fractional width).
36
+ expect(chunkReviewPercent(subtasks({ total: 3, completed: 1 }))).toBe(33)
37
+ })
38
+
39
+ it('clamps to 0..100 even if counts are inconsistent', () => {
40
+ expect(chunkReviewPercent(subtasks({ total: 2, completed: 5 }))).toBe(100)
41
+ expect(chunkReviewPercent(subtasks({ total: 2, completed: -1 }))).toBe(0)
42
+ })
43
+ })
44
+
45
+ describe('activeChunkLabels', () => {
46
+ it('returns only the in-progress chunk labels, in order', () => {
47
+ const s = subtasks({
48
+ total: 3,
49
+ completed: 1,
50
+ inProgress: 1,
51
+ items: [
52
+ { label: 'auth', status: 'completed' },
53
+ { label: 'db layer', status: 'in_progress' },
54
+ { label: 'ui', status: 'pending' },
55
+ ],
56
+ })
57
+ expect(activeChunkLabels(s)).toEqual(['db layer'])
58
+ })
59
+
60
+ it('is empty with no items or no in-progress chunk', () => {
61
+ expect(activeChunkLabels(null)).toEqual([])
62
+ expect(activeChunkLabels(subtasks({ total: 2 }))).toEqual([])
63
+ expect(
64
+ activeChunkLabels(
65
+ subtasks({
66
+ total: 1,
67
+ completed: 1,
68
+ items: [{ label: 'only', status: 'completed' }],
69
+ }),
70
+ ),
71
+ ).toEqual([])
72
+ })
73
+ })
@@ -0,0 +1,28 @@
1
+ import type { StepSubtasks } from '~/types/execution'
2
+
3
+ // Pure derivation of the PR deep-reviewer's `reviewing`-phase progress, factored out of
4
+ // `PrReviewWindow.vue` so it is unit-testable independently of the Vue component.
5
+ //
6
+ // The reviewer maintains a per-slice todo list (`step.subtasks`) once it has grouped the diff
7
+ // into cohesive chunks. The PRESENCE of that list is the signal that slicing is done, so the two
8
+ // `reviewing` sub-phases are told apart by it:
9
+ // - no todo list yet (`total === 0`) → still SLICING the diff into chunks (no plan committed).
10
+ // - todo list present (`total > 0`) → slicing DONE, working through the chunks.
11
+
12
+ /** True while the reviewer is still grouping the diff into chunks (it has not committed a plan). */
13
+ export function isSlicingChunks(subtasks: StepSubtasks | null | undefined): boolean {
14
+ return (subtasks?.total ?? 0) <= 0
15
+ }
16
+
17
+ /** Chunk-review completion as an integer percent, clamped 0..100, for the progress bar. */
18
+ export function chunkReviewPercent(subtasks: StepSubtasks | null | undefined): number {
19
+ const total = subtasks?.total ?? 0
20
+ if (total <= 0) return 0
21
+ const completed = subtasks?.completed ?? 0
22
+ return Math.min(100, Math.max(0, Math.round((completed / total) * 100)))
23
+ }
24
+
25
+ /** Labels of the chunk(s) the reviewer is actively working through right now (for the callout). */
26
+ export function activeChunkLabels(subtasks: StepSubtasks | null | undefined): string[] {
27
+ return subtasks?.items?.filter((i) => i.status === 'in_progress').map((i) => i.label) ?? []
28
+ }
@@ -4943,10 +4943,39 @@
4943
4943
  "suggestedFix": "Lösungsvorschlag:",
4944
4944
  "line": "Zeile {line}",
4945
4945
  "reviewing": {
4946
- "title": "Pull Request wird geprüft…",
4947
- "hint": "Der Diff wird in zusammenhängende Teile zerlegt und einzeln geprüft.",
4946
+ "slicing": {
4947
+ "title": "Diff wird in Abschnitte zerlegt",
4948
+ "hint": "Die geänderten Dateien werden in zusammenhängende Abschnitte gruppiert, damit jeder einzeln geprüft werden kann. Die Abschnittsliste erscheint hier, sobald die Zerlegung abgeschlossen ist."
4949
+ },
4950
+ "reviewingChunks": {
4951
+ "title": "Abschnitte werden geprüft…",
4952
+ "hint": "Die Zerlegung ist abgeschlossen. Jeder Abschnitt wird nacheinander geprüft und die Ergebnisse werden gesammelt."
4953
+ },
4948
4954
  "chunks": "{completed} von {total} Abschnitten geprüft",
4949
- "inProgress": "{count} in Bearbeitung"
4955
+ "inProgress": "{count} in Bearbeitung",
4956
+ "activeHeading": "Wird jetzt geprüft",
4957
+ "chunksHeading": "Abschnitte",
4958
+ "chunkStatus": {
4959
+ "completed": "Geprüft",
4960
+ "in_progress": "Wird geprüft…",
4961
+ "pending": "In Warteschlange"
4962
+ }
4963
+ },
4964
+ "challenge": {
4965
+ "action": "Anfechten",
4966
+ "reChallenge": "Erneut anfechten",
4967
+ "dismiss": "Verwerfen",
4968
+ "submit": "Untersuchen",
4969
+ "placeholder": "Optional: Was genau ist an diesem Befund falsch oder nicht überzeugend?",
4970
+ "hint": "Leer lassen, damit der Prüfer tiefer gräbt, den Befund begründet und prüft, ob er zutreffend und relevant ist.",
4971
+ "investigatingBanner": "Der Challenge-Investigator prüft einen Befund erneut anhand des Quellcodes…",
4972
+ "investigatingBadge": "Wird untersucht",
4973
+ "strengthenedBadge": "Bestärkt",
4974
+ "upheldBadge": "Bestätigt",
4975
+ "retractedBadge": "Zurückgezogen",
4976
+ "verdictLabel": "Prüfer:",
4977
+ "failedBadge": "Anfechtung fehlgeschlagen",
4978
+ "failedLabel": "Anfechtung fehlgeschlagen:"
4950
4979
  },
4951
4980
  "fixing": {
4952
4981
  "title": "Ausgewählte Befunde werden behoben…",
@@ -5072,10 +5072,39 @@
5072
5072
  "suggestedFix": "Suggested fix:",
5073
5073
  "line": "line {line}",
5074
5074
  "reviewing": {
5075
- "title": "Reviewing the pull request…",
5076
- "hint": "Slicing the diff into cohesive chunks and reviewing each one.",
5075
+ "slicing": {
5076
+ "title": "Slicing the diff into chunks",
5077
+ "hint": "Grouping the changed files into cohesive chunks so each can be reviewed on its own. The chunk list appears here once slicing is done."
5078
+ },
5079
+ "reviewingChunks": {
5080
+ "title": "Reviewing the chunks…",
5081
+ "hint": "Slicing is done. Working through each chunk one at a time and collecting findings."
5082
+ },
5077
5083
  "chunks": "{completed} of {total} chunks reviewed",
5078
- "inProgress": "{count} in progress"
5084
+ "inProgress": "{count} in progress",
5085
+ "activeHeading": "Reviewing now",
5086
+ "chunksHeading": "Chunks",
5087
+ "chunkStatus": {
5088
+ "completed": "Reviewed",
5089
+ "in_progress": "Reviewing…",
5090
+ "pending": "Queued"
5091
+ }
5092
+ },
5093
+ "challenge": {
5094
+ "action": "Challenge",
5095
+ "reChallenge": "Challenge again",
5096
+ "dismiss": "Dismiss",
5097
+ "submit": "Investigate",
5098
+ "placeholder": "Optional: what specifically is wrong or unconvincing about this finding?",
5099
+ "hint": "Leave blank to have the investigator dig deeper, justify the finding, and check it's accurate and relevant.",
5100
+ "investigatingBanner": "The Challenge Investigator is re-examining a finding against the source…",
5101
+ "investigatingBadge": "Investigating",
5102
+ "strengthenedBadge": "Strengthened",
5103
+ "upheldBadge": "Upheld",
5104
+ "retractedBadge": "Retracted",
5105
+ "verdictLabel": "Investigator:",
5106
+ "failedBadge": "Challenge failed",
5107
+ "failedLabel": "Challenge failed:"
5079
5108
  },
5080
5109
  "fixing": {
5081
5110
  "title": "Fixing the selected findings…",
@@ -4931,10 +4931,39 @@
4931
4931
  "suggestedFix": "Corrección sugerida:",
4932
4932
  "line": "línea {line}",
4933
4933
  "reviewing": {
4934
- "title": "Revisando el pull request…",
4935
- "hint": "Dividiendo el diff en bloques coherentes y revisando cada uno.",
4934
+ "slicing": {
4935
+ "title": "Dividiendo el diff en fragmentos…",
4936
+ "hint": "Agrupando los archivos modificados en fragmentos coherentes para revisar cada uno por separado. La lista de fragmentos aparecerá aquí cuando termine la división."
4937
+ },
4938
+ "reviewingChunks": {
4939
+ "title": "Revisando los fragmentos…",
4940
+ "hint": "La división ha terminado. Revisando cada fragmento de uno en uno y recopilando los hallazgos."
4941
+ },
4936
4942
  "chunks": "{completed} de {total} fragmentos revisados",
4937
- "inProgress": "{count} en curso"
4943
+ "inProgress": "{count} en curso",
4944
+ "activeHeading": "Revisando ahora",
4945
+ "chunksHeading": "Fragmentos",
4946
+ "chunkStatus": {
4947
+ "completed": "Revisado",
4948
+ "in_progress": "Revisando…",
4949
+ "pending": "En cola"
4950
+ }
4951
+ },
4952
+ "challenge": {
4953
+ "action": "Cuestionar",
4954
+ "reChallenge": "Cuestionar de nuevo",
4955
+ "dismiss": "Descartar",
4956
+ "submit": "Investigar",
4957
+ "placeholder": "Opcional: ¿qué es exactamente lo incorrecto o poco convincente de este hallazgo?",
4958
+ "hint": "Déjalo en blanco para que el investigador profundice, justifique el hallazgo y compruebe que es preciso y relevante.",
4959
+ "investigatingBanner": "El Investigador de Objeciones está reexaminando un hallazgo frente al código fuente…",
4960
+ "investigatingBadge": "Investigando",
4961
+ "strengthenedBadge": "Reforzado",
4962
+ "upheldBadge": "Confirmado",
4963
+ "retractedBadge": "Retirado",
4964
+ "verdictLabel": "Investigador:",
4965
+ "failedBadge": "Cuestionamiento fallido",
4966
+ "failedLabel": "Cuestionamiento fallido:"
4938
4967
  },
4939
4968
  "fixing": {
4940
4969
  "title": "Corrigiendo los hallazgos seleccionados…",
@@ -4931,10 +4931,39 @@
4931
4931
  "suggestedFix": "Correction suggérée :",
4932
4932
  "line": "ligne {line}",
4933
4933
  "reviewing": {
4934
- "title": "Revue de la pull request…",
4935
- "hint": "Découpage du diff en blocs cohérents et revue de chacun.",
4934
+ "slicing": {
4935
+ "title": "Découpage du diff en sections…",
4936
+ "hint": "Regroupement des fichiers modifiés en sections cohérentes afin de pouvoir examiner chacune séparément. La liste des sections apparaîtra ici une fois le découpage terminé."
4937
+ },
4938
+ "reviewingChunks": {
4939
+ "title": "Revue des sections…",
4940
+ "hint": "Le découpage est terminé. Chaque section est examinée l'une après l'autre et les remarques sont rassemblées."
4941
+ },
4936
4942
  "chunks": "{completed} sur {total} sections examinées",
4937
- "inProgress": "{count} en cours"
4943
+ "inProgress": "{count} en cours",
4944
+ "activeHeading": "Revue en cours",
4945
+ "chunksHeading": "Sections",
4946
+ "chunkStatus": {
4947
+ "completed": "Examinée",
4948
+ "in_progress": "En cours d'examen…",
4949
+ "pending": "En attente"
4950
+ }
4951
+ },
4952
+ "challenge": {
4953
+ "action": "Contester",
4954
+ "reChallenge": "Contester à nouveau",
4955
+ "dismiss": "Écarter",
4956
+ "submit": "Enquêter",
4957
+ "placeholder": "Facultatif : qu'y a-t-il précisément d'incorrect ou de peu convaincant dans ce constat ?",
4958
+ "hint": "Laissez vide pour que l'enquêteur approfondisse, justifie le constat et vérifie qu'il est exact et pertinent.",
4959
+ "investigatingBanner": "L'enquêteur de contestation réexamine un constat par rapport au code source…",
4960
+ "investigatingBadge": "Enquête en cours",
4961
+ "strengthenedBadge": "Renforcé",
4962
+ "upheldBadge": "Confirmé",
4963
+ "retractedBadge": "Retiré",
4964
+ "verdictLabel": "Enquêteur :",
4965
+ "failedBadge": "Échec de la contestation",
4966
+ "failedLabel": "Échec de la contestation :"
4938
4967
  },
4939
4968
  "fixing": {
4940
4969
  "title": "Correction des points sélectionnés…",
@@ -4942,10 +4942,39 @@
4942
4942
  "suggestedFix": "תיקון מוצע:",
4943
4943
  "line": "שורה {line}",
4944
4944
  "reviewing": {
4945
- "title": "בודק את בקשת המשיכה…",
4946
- "hint": "מחלק את ההבדלים לקטעים לכידים ובודק כל אחד.",
4945
+ "slicing": {
4946
+ "title": "מחלק את ההבדלים למקטעים…",
4947
+ "hint": "מקבץ את הקבצים שהשתנו למקטעים לכידים כדי לבדוק כל אחד בנפרד. רשימת המקטעים תופיע כאן לאחר סיום החלוקה."
4948
+ },
4949
+ "reviewingChunks": {
4950
+ "title": "בודק את המקטעים…",
4951
+ "hint": "החלוקה הסתיימה. בודק כל מקטע בנפרד ואוסף את הממצאים."
4952
+ },
4947
4953
  "chunks": "{completed} מתוך {total} מקטעים נבדקו",
4948
- "inProgress": "{count} בתהליך"
4954
+ "inProgress": "{count} בתהליך",
4955
+ "activeHeading": "נבדק כעת",
4956
+ "chunksHeading": "מקטעים",
4957
+ "chunkStatus": {
4958
+ "completed": "נבדק",
4959
+ "in_progress": "בבדיקה…",
4960
+ "pending": "בתור"
4961
+ }
4962
+ },
4963
+ "challenge": {
4964
+ "action": "ערער",
4965
+ "reChallenge": "ערער שוב",
4966
+ "dismiss": "התעלם",
4967
+ "submit": "חקור",
4968
+ "placeholder": "רשות: מה בדיוק שגוי או לא משכנע בממצא הזה?",
4969
+ "hint": "השאירו ריק כדי שהחוקר יעמיק, ינמק את הממצא ויוודא שהוא מדויק ורלוונטי.",
4970
+ "investigatingBanner": "חוקר הערעורים בוחן מחדש ממצא מול קוד המקור…",
4971
+ "investigatingBadge": "בחקירה",
4972
+ "strengthenedBadge": "חוזק",
4973
+ "upheldBadge": "אושר",
4974
+ "retractedBadge": "בוטל",
4975
+ "verdictLabel": "חוקר:",
4976
+ "failedBadge": "הערעור נכשל",
4977
+ "failedLabel": "הערעור נכשל:"
4949
4978
  },
4950
4979
  "fixing": {
4951
4980
  "title": "מתקן את הממצאים שנבחרו…",
@@ -4943,10 +4943,39 @@
4943
4943
  "suggestedFix": "Correzione suggerita:",
4944
4944
  "line": "riga {line}",
4945
4945
  "reviewing": {
4946
- "title": "Revisione della pull request…",
4947
- "hint": "Suddivisione del diff in blocchi coerenti e revisione di ciascuno.",
4946
+ "slicing": {
4947
+ "title": "Suddivisione del diff in blocchi",
4948
+ "hint": "Raggruppamento dei file modificati in blocchi coerenti per esaminare ciascuno separatamente. L'elenco dei blocchi comparirà qui al termine della suddivisione."
4949
+ },
4950
+ "reviewingChunks": {
4951
+ "title": "Revisione dei blocchi…",
4952
+ "hint": "La suddivisione è terminata. Ogni blocco viene esaminato uno alla volta e le osservazioni vengono raccolte."
4953
+ },
4948
4954
  "chunks": "{completed} di {total} sezioni esaminate",
4949
- "inProgress": "{count} in corso"
4955
+ "inProgress": "{count} in corso",
4956
+ "activeHeading": "In revisione ora",
4957
+ "chunksHeading": "Blocchi",
4958
+ "chunkStatus": {
4959
+ "completed": "Esaminato",
4960
+ "in_progress": "In revisione…",
4961
+ "pending": "In coda"
4962
+ }
4963
+ },
4964
+ "challenge": {
4965
+ "action": "Contesta",
4966
+ "reChallenge": "Contesta di nuovo",
4967
+ "dismiss": "Scarta",
4968
+ "submit": "Indaga",
4969
+ "placeholder": "Facoltativo: cosa c'è di preciso di sbagliato o poco convincente in questo rilievo?",
4970
+ "hint": "Lascia vuoto per far approfondire l'investigatore, giustificare il rilievo e verificarne accuratezza e pertinenza.",
4971
+ "investigatingBanner": "L'investigatore delle contestazioni sta riesaminando un rilievo rispetto al codice sorgente…",
4972
+ "investigatingBadge": "In esame",
4973
+ "strengthenedBadge": "Rafforzato",
4974
+ "upheldBadge": "Confermato",
4975
+ "retractedBadge": "Ritirato",
4976
+ "verdictLabel": "Investigatore:",
4977
+ "failedBadge": "Contestazione non riuscita",
4978
+ "failedLabel": "Contestazione non riuscita:"
4950
4979
  },
4951
4980
  "fixing": {
4952
4981
  "title": "Correzione dei rilievi selezionati…",
@@ -4943,10 +4943,39 @@
4943
4943
  "suggestedFix": "修正案:",
4944
4944
  "line": "{line}行目",
4945
4945
  "reviewing": {
4946
- "title": "プルリクエストをレビュー中…",
4947
- "hint": "差分をまとまりのある単位に分割し、各単位をレビューしています。",
4946
+ "slicing": {
4947
+ "title": "差分をチャンクに分割中…",
4948
+ "hint": "変更されたファイルをまとまりのあるチャンクにグループ化し、各チャンクを個別にレビューできるようにしています。分割が完了すると、ここにチャンク一覧が表示されます。"
4949
+ },
4950
+ "reviewingChunks": {
4951
+ "title": "チャンクをレビュー中…",
4952
+ "hint": "分割は完了しました。各チャンクを一つずつレビューし、指摘事項を収集しています。"
4953
+ },
4948
4954
  "chunks": "{total} 個中 {completed} 個のチャンクをレビュー済み",
4949
- "inProgress": "{count} 件処理中"
4955
+ "inProgress": "{count} 件処理中",
4956
+ "activeHeading": "レビュー中",
4957
+ "chunksHeading": "チャンク",
4958
+ "chunkStatus": {
4959
+ "completed": "レビュー済み",
4960
+ "in_progress": "レビュー中…",
4961
+ "pending": "待機中"
4962
+ }
4963
+ },
4964
+ "challenge": {
4965
+ "action": "異議を唱える",
4966
+ "reChallenge": "再度異議を唱える",
4967
+ "dismiss": "却下",
4968
+ "submit": "調査する",
4969
+ "placeholder": "任意: この指摘のどこが誤り、または納得できないかを記入してください。",
4970
+ "hint": "空欄のままにすると、調査担当がさらに深く掘り下げ、指摘の根拠を示し、正確性と関連性を検証します。",
4971
+ "investigatingBanner": "異議調査担当がソースコードに照らして指摘を再検証しています…",
4972
+ "investigatingBadge": "調査中",
4973
+ "strengthenedBadge": "強化済み",
4974
+ "upheldBadge": "支持",
4975
+ "retractedBadge": "撤回済み",
4976
+ "verdictLabel": "調査担当:",
4977
+ "failedBadge": "異議調査に失敗",
4978
+ "failedLabel": "異議調査に失敗:"
4950
4979
  },
4951
4980
  "fixing": {
4952
4981
  "title": "選択した指摘を修正中…",
@@ -4931,10 +4931,39 @@
4931
4931
  "suggestedFix": "Sugerowana poprawka:",
4932
4932
  "line": "wiersz {line}",
4933
4933
  "reviewing": {
4934
- "title": "Przeglądanie pull requesta…",
4935
- "hint": "Dzielenie zmian na spójne części i przeglądanie każdej z nich.",
4934
+ "slicing": {
4935
+ "title": "Dzielenie zmian na fragmenty…",
4936
+ "hint": "Grupowanie zmienionych plików w spójne fragmenty, aby każdy można było przejrzeć osobno. Lista fragmentów pojawi się tutaj po zakończeniu podziału."
4937
+ },
4938
+ "reviewingChunks": {
4939
+ "title": "Przeglądanie fragmentów…",
4940
+ "hint": "Podział zakończony. Przeglądanie każdego fragmentu po kolei i zbieranie uwag."
4941
+ },
4936
4942
  "chunks": "Sprawdzono {completed} z {total} fragmentów",
4937
- "inProgress": "{count} w toku"
4943
+ "inProgress": "{count} w toku",
4944
+ "activeHeading": "Przeglądane teraz",
4945
+ "chunksHeading": "Fragmenty",
4946
+ "chunkStatus": {
4947
+ "completed": "Sprawdzony",
4948
+ "in_progress": "Przeglądanie…",
4949
+ "pending": "W kolejce"
4950
+ }
4951
+ },
4952
+ "challenge": {
4953
+ "action": "Zakwestionuj",
4954
+ "reChallenge": "Zakwestionuj ponownie",
4955
+ "dismiss": "Odrzuć",
4956
+ "submit": "Zbadaj",
4957
+ "placeholder": "Opcjonalnie: co dokładnie jest błędne lub nieprzekonujące w tym ustaleniu?",
4958
+ "hint": "Pozostaw puste, aby badacz zgłębił temat, uzasadnił ustalenie i sprawdził, czy jest trafne i istotne.",
4959
+ "investigatingBanner": "Badacz zastrzeżeń ponownie analizuje ustalenie w oparciu o kod źródłowy…",
4960
+ "investigatingBadge": "Badanie",
4961
+ "strengthenedBadge": "Wzmocnione",
4962
+ "upheldBadge": "Podtrzymane",
4963
+ "retractedBadge": "Wycofane",
4964
+ "verdictLabel": "Badacz:",
4965
+ "failedBadge": "Zakwestionowanie nie powiodło się",
4966
+ "failedLabel": "Zakwestionowanie nie powiodło się:"
4938
4967
  },
4939
4968
  "fixing": {
4940
4969
  "title": "Naprawianie wybranych uwag…",
@@ -4943,10 +4943,39 @@
4943
4943
  "suggestedFix": "Önerilen düzeltme:",
4944
4944
  "line": "satır {line}",
4945
4945
  "reviewing": {
4946
- "title": "Pull request inceleniyor…",
4947
- "hint": "Fark tutarlı parçalara bölünüp her biri inceleniyor.",
4946
+ "slicing": {
4947
+ "title": "Fark parçalara bölünüyor…",
4948
+ "hint": "Değiştirilen dosyalar, her biri ayrı ayrı incelenebilsin diye tutarlı parçalara gruplanıyor. Bölme tamamlandığında parça listesi burada görünecek."
4949
+ },
4950
+ "reviewingChunks": {
4951
+ "title": "Parçalar inceleniyor…",
4952
+ "hint": "Bölme tamamlandı. Her parça tek tek inceleniyor ve bulgular toplanıyor."
4953
+ },
4948
4954
  "chunks": "{total} bölümden {completed} tanesi incelendi",
4949
- "inProgress": "{count} devam ediyor"
4955
+ "inProgress": "{count} devam ediyor",
4956
+ "activeHeading": "Şimdi inceleniyor",
4957
+ "chunksHeading": "Parçalar",
4958
+ "chunkStatus": {
4959
+ "completed": "İncelendi",
4960
+ "in_progress": "İnceleniyor…",
4961
+ "pending": "Sırada"
4962
+ }
4963
+ },
4964
+ "challenge": {
4965
+ "action": "İtiraz et",
4966
+ "reChallenge": "Tekrar itiraz et",
4967
+ "dismiss": "Yoksay",
4968
+ "submit": "İncele",
4969
+ "placeholder": "İsteğe bağlı: Bu bulguda tam olarak yanlış veya ikna edici olmayan nedir?",
4970
+ "hint": "Araştırmacının daha derine inmesi, bulguyu gerekçelendirmesi ve doğru ve ilgili olduğunu doğrulaması için boş bırakın.",
4971
+ "investigatingBanner": "İtiraz Araştırmacısı bir bulguyu kaynak koda göre yeniden inceliyor…",
4972
+ "investigatingBadge": "İnceleniyor",
4973
+ "strengthenedBadge": "Güçlendirildi",
4974
+ "upheldBadge": "Onaylandı",
4975
+ "retractedBadge": "Geri çekildi",
4976
+ "verdictLabel": "Araştırmacı:",
4977
+ "failedBadge": "İtiraz başarısız oldu",
4978
+ "failedLabel": "İtiraz başarısız oldu:"
4950
4979
  },
4951
4980
  "fixing": {
4952
4981
  "title": "Seçili bulgular düzeltiliyor…",
@@ -4931,10 +4931,39 @@
4931
4931
  "suggestedFix": "Пропоноване виправлення:",
4932
4932
  "line": "рядок {line}",
4933
4933
  "reviewing": {
4934
- "title": "Перевірка pull request…",
4935
- "hint": "Поділ змін на цілісні частини та огляд кожної з них.",
4934
+ "slicing": {
4935
+ "title": "Поділ змін на фрагменти…",
4936
+ "hint": "Групування змінених файлів у цілісні фрагменти, щоб кожен можна було переглянути окремо. Список фрагментів з'явиться тут після завершення поділу."
4937
+ },
4938
+ "reviewingChunks": {
4939
+ "title": "Огляд фрагментів…",
4940
+ "hint": "Поділ завершено. Кожен фрагмент переглядається по черзі, а зауваження збираються."
4941
+ },
4936
4942
  "chunks": "Перевірено {completed} з {total} фрагментів",
4937
- "inProgress": "{count} у процесі"
4943
+ "inProgress": "{count} у процесі",
4944
+ "activeHeading": "Переглядається зараз",
4945
+ "chunksHeading": "Фрагменти",
4946
+ "chunkStatus": {
4947
+ "completed": "Переглянуто",
4948
+ "in_progress": "Огляд…",
4949
+ "pending": "У черзі"
4950
+ }
4951
+ },
4952
+ "challenge": {
4953
+ "action": "Оскаржити",
4954
+ "reChallenge": "Оскаржити знову",
4955
+ "dismiss": "Відхилити",
4956
+ "submit": "Дослідити",
4957
+ "placeholder": "Необов'язково: що саме неправильне або непереконливе в цьому зауваженні?",
4958
+ "hint": "Залиште порожнім, щоб дослідник копнув глибше, обґрунтував зауваження та перевірив його точність і доречність.",
4959
+ "investigatingBanner": "Дослідник оскаржень повторно перевіряє зауваження за вихідним кодом…",
4960
+ "investigatingBadge": "Дослідження",
4961
+ "strengthenedBadge": "Посилено",
4962
+ "upheldBadge": "Підтверджено",
4963
+ "retractedBadge": "Відкликано",
4964
+ "verdictLabel": "Дослідник:",
4965
+ "failedBadge": "Оскарження не вдалося",
4966
+ "failedLabel": "Оскарження не вдалося:"
4938
4967
  },
4939
4968
  "fixing": {
4940
4969
  "title": "Виправлення вибраних зауважень…",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.146.3",
3
+ "version": "0.147.1",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.153.0"
43
+ "@cat-factory/contracts": "0.154.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",