@cat-factory/app 0.117.0 → 0.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,9 @@ const META: Record<Notification['type'], { icon: string; color: Accent }> = {
64
64
  // the title opens the fork-decision window (see `reveal`); "act" just marks it read (the
65
65
  // choice is made in that window — pick a fork / enter a custom approach — not here).
66
66
  fork_decision_pending: { icon: 'i-lucide-git-fork', color: 'warning' },
67
+ // The PR reviewer surfaced findings to triage. Clicking the title opens the PR-review window
68
+ // (see `reveal`); "act" just marks it read (findings are selected in that window, not here).
69
+ pr_review_ready: { icon: 'i-lucide-clipboard-check', color: 'primary' },
67
70
  // The initiative loop needs attention (a blocked task, or completion). Clicking the title
68
71
  // opens the initiative tracker window; "act" just marks it read.
69
72
  initiative: { icon: 'i-lucide-milestone', color: 'primary' },
@@ -86,6 +89,7 @@ const ACTION_KEYS: Record<Notification['type'], string> = {
86
89
  human_review: 'layout.notifications.action.human_review',
87
90
  followup_pending: 'layout.notifications.action.followup_pending',
88
91
  fork_decision_pending: 'layout.notifications.action.fork_decision_pending',
92
+ pr_review_ready: 'layout.notifications.action.pr_review_ready',
89
93
  initiative: 'layout.notifications.action.initiative',
90
94
  }
91
95
 
@@ -157,6 +161,7 @@ function reveal(n: Notification) {
157
161
  else if (n.type === 'human_review') revealHumanReview(n)
158
162
  else if (n.type === 'followup_pending') revealFollowUps(n)
159
163
  else if (n.type === 'fork_decision_pending') revealForkDecision(n)
164
+ else if (n.type === 'pr_review_ready') revealPrReview(n)
160
165
  else if (n.type === 'initiative') ui.openInitiativeTracker(n.blockId)
161
166
  else ui.select(n.blockId)
162
167
  }
@@ -191,6 +196,15 @@ function revealForkDecision(n: Notification) {
191
196
  else if (n.blockId) ui.select(n.blockId)
192
197
  }
193
198
 
199
+ /**
200
+ * Open the PR deep-review window for a run parked awaiting a finding selection.
201
+ * Falls back to focusing the block when the run isn't loaded.
202
+ */
203
+ function revealPrReview(n: Notification) {
204
+ if (n.executionId && execution.getInstance(n.executionId)) ui.openPrReview(n.executionId)
205
+ else if (n.blockId) ui.select(n.blockId)
206
+ }
207
+
194
208
  /**
195
209
  * Open the human-testing window for a parked `human-test` gate: find the run's parked
196
210
  * human-test step and open it through the universal step dispatch (its archetype declares
@@ -23,6 +23,7 @@ import GenericStructuredResultView from '~/components/panels/GenericStructuredRe
23
23
  import ServiceSpecWindow from '~/components/spec/ServiceSpecWindow.vue'
24
24
  import FollowUpWindow from '~/components/followUp/FollowUpWindow.vue'
25
25
  import ForkDecisionWindow from '~/components/forkDecision/ForkDecisionWindow.vue'
26
+ import PrReviewWindow from '~/components/prReview/PrReviewWindow.vue'
26
27
  import MergerResultView from '~/components/panels/MergerResultView.vue'
27
28
  import InitiativeTrackerWindow from '~/components/initiative/InitiativeTrackerWindow.vue'
28
29
  import InitiativePlanningWindow from '~/components/initiative/InitiativePlanningWindow.vue'
@@ -57,6 +58,9 @@ const STEP_RESULT_VIEWS: Record<string, Component> = {
57
58
  // human's pick / custom approach. Opened directly via `ui.openForkDecision` (the pipeline
58
59
  // chip, the inspector button, and the `fork_decision_pending` card).
59
60
  'fork-decision': ForkDecisionWindow,
61
+ // The PR deep-review: the reviewer's sliced, prioritized findings + the human's multi-select.
62
+ // Opened as the `pr-reviewer` step's result view and via the `pr_review_ready` inbox card.
63
+ 'pr-review': PrReviewWindow,
60
64
  // The merger's verdict: the PR's complexity/risk/impact scores + the engine's auto-merge
61
65
  // or awaiting-review decision (and why), instead of the agent's raw JSON.
62
66
  merger: MergerResultView,
@@ -0,0 +1,289 @@
1
+ <script setup lang="ts">
2
+ // PR deep-review window — the dedicated surface for the read-only `pr-reviewer`'s sliced,
3
+ // prioritized findings, opened via the universal result-view host. It reads the live review
4
+ // state straight off the run's `pr-reviewer` step (`step.prReview`, kept fresh by the
5
+ // execution stream) and lets a human multi-SELECT which findings matter, grouped by slice and
6
+ // sorted by severity, then finish the review. The Fixer / inline-comment resolutions are the
7
+ // tracked PR 3 follow-up; this window's `Finish review` records the curated selection.
8
+ import { computed, ref, watch } from 'vue'
9
+ import { useResultView } from '~/composables/useResultView'
10
+ import { useExecutionStore } from '~/stores/execution'
11
+ import { useBoardStore } from '~/stores/board'
12
+ import { usePrReviewStore } from '~/stores/prReview'
13
+ import type { PrReviewFinding, PrReviewSeverity, PrReviewStepState } from '~/types/execution'
14
+
15
+ const execution = useExecutionStore()
16
+ const board = useBoardStore()
17
+ const prReview = usePrReviewStore()
18
+
19
+ const { t } = useI18n()
20
+
21
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('pr-review', {
22
+ onOpen: (_id) => {
23
+ if (instanceId.value) void prReview.load(instanceId.value)
24
+ },
25
+ })
26
+
27
+ const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
28
+ const instance = computed(() =>
29
+ instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
30
+ )
31
+ const step = computed(() => {
32
+ if (instance.value === null || stepIndex.value === null) return null
33
+ return instance.value.steps[stepIndex.value] ?? null
34
+ })
35
+ const state = computed<PrReviewStepState | null>(() => step.value?.prReview ?? null)
36
+ const status = computed(() => state.value?.status ?? null)
37
+ const awaiting = computed(() => status.value === 'awaiting_selection')
38
+ const findings = computed<PrReviewFinding[]>(() => state.value?.findings ?? [])
39
+
40
+ /** Severity → chip classes (styling, not copy). */
41
+ const SEVERITY_CLASS: Record<PrReviewSeverity, string> = {
42
+ blocker: 'bg-rose-500/15 text-rose-300 ring-rose-500/30',
43
+ high: 'bg-orange-500/15 text-orange-300 ring-orange-500/30',
44
+ medium: 'bg-amber-500/15 text-amber-300 ring-amber-500/30',
45
+ low: 'bg-sky-500/15 text-sky-300 ring-sky-500/30',
46
+ nit: 'bg-slate-500/15 text-slate-300 ring-slate-500/30',
47
+ }
48
+
49
+ /** Findings grouped under their slice (in the review's slice order), plus an "Other" bucket. */
50
+ const groups = computed(() => {
51
+ const slices = state.value?.slices ?? []
52
+ const byId = new Map<string, PrReviewFinding[]>()
53
+ const unsliced: PrReviewFinding[] = []
54
+ for (const f of findings.value) {
55
+ if (f.sliceId && slices.some((s) => s.id === f.sliceId)) {
56
+ const arr = byId.get(f.sliceId) ?? []
57
+ arr.push(f)
58
+ byId.set(f.sliceId, arr)
59
+ } else {
60
+ unsliced.push(f)
61
+ }
62
+ }
63
+ const out = slices
64
+ .map((s) => ({ id: s.id, title: s.title, rationale: s.rationale, items: byId.get(s.id) ?? [] }))
65
+ .filter((g) => g.items.length > 0)
66
+ if (unsliced.length > 0) {
67
+ out.push({ id: '__unsliced', title: t('prReview.unsliced'), rationale: '', items: unsliced })
68
+ }
69
+ return out
70
+ })
71
+
72
+ // The human's selection — a set of finding ids. Defaults to every finding (the human deselects
73
+ // the noise), so "Finish" without touching anything keeps all. Re-seeded ONLY when the set of
74
+ // finding IDS actually changes — NOT on every execution-stream re-emit (which hands us a fresh
75
+ // `findings` array reference on each reconnect/resync). Keying on the id set keeps the human's
76
+ // in-progress curation from being silently reset by an unrelated live update.
77
+ const findingIdKey = computed(() => findings.value.map((f) => f.id).join('\n'))
78
+ const selected = ref<Set<string>>(new Set())
79
+ watch(
80
+ findingIdKey,
81
+ () => {
82
+ selected.value = new Set(findings.value.map((f) => f.id))
83
+ },
84
+ { immediate: true },
85
+ )
86
+
87
+ function toggle(id: string): void {
88
+ const next = new Set(selected.value)
89
+ if (next.has(id)) next.delete(id)
90
+ else next.add(id)
91
+ selected.value = next
92
+ }
93
+ function selectAll(): void {
94
+ selected.value = new Set(findings.value.map((f) => f.id))
95
+ }
96
+ function clearAll(): void {
97
+ selected.value = new Set()
98
+ }
99
+
100
+ const canFinish = computed(() => awaiting.value && !prReview.resolving)
101
+
102
+ async function onFinish(): Promise<void> {
103
+ const id = instanceId.value
104
+ if (!id || !canFinish.value) return
105
+ await prReview.resolve(id, [...selected.value]).catch(() => {})
106
+ }
107
+ </script>
108
+
109
+ <template>
110
+ <Teleport to="body">
111
+ <div
112
+ v-if="open"
113
+ data-testid="pr-review-window"
114
+ class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
115
+ @click.self="close"
116
+ >
117
+ <div
118
+ class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
119
+ role="dialog"
120
+ aria-modal="true"
121
+ >
122
+ <!-- Header -->
123
+ <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
124
+ <span
125
+ class="flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-300"
126
+ >
127
+ <UIcon name="i-lucide-clipboard-check" class="h-4 w-4" />
128
+ </span>
129
+ <div class="min-w-0 flex-1">
130
+ <h2 class="truncate text-sm font-semibold text-slate-100">
131
+ {{
132
+ block ? t('prReview.titleWithBlock', { title: block.title }) : t('prReview.title')
133
+ }}
134
+ </h2>
135
+ <p class="truncate text-[11px] text-slate-400">{{ t('prReview.subtitle') }}</p>
136
+ </div>
137
+ <a
138
+ v-if="state?.prUrl"
139
+ :href="state.prUrl"
140
+ target="_blank"
141
+ rel="noopener"
142
+ class="rounded-md px-2 py-1 text-[11px] text-indigo-300 hover:bg-slate-800"
143
+ >
144
+ {{ t('prReview.openPr') }}
145
+ </a>
146
+ <button
147
+ class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
148
+ @click="close"
149
+ >
150
+ <UIcon name="i-lucide-x" class="h-4 w-4" />
151
+ </button>
152
+ </header>
153
+
154
+ <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
155
+ <!-- Reviewing: the read-only reviewer is still working. -->
156
+ <div
157
+ v-if="status === 'reviewing'"
158
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
159
+ >
160
+ <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
161
+ <p class="text-sm">{{ t('prReview.reviewing.title') }}</p>
162
+ <p class="max-w-sm text-[11px] text-slate-500">{{ t('prReview.reviewing.hint') }}</p>
163
+ </div>
164
+
165
+ <template v-else>
166
+ <p
167
+ v-if="prReview.error"
168
+ class="mb-3 rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
169
+ >
170
+ {{ prReview.error }}
171
+ </p>
172
+
173
+ <!-- The reviewer's overall assessment. -->
174
+ <p
175
+ v-if="state?.summary"
176
+ class="mb-3 rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
177
+ >
178
+ <span class="text-slate-500">{{ t('prReview.summaryLabel') }}</span>
179
+ {{ state.summary }}
180
+ </p>
181
+
182
+ <!-- A clean PR / resolved review with no findings. -->
183
+ <div
184
+ v-if="findings.length === 0"
185
+ class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-6 text-center text-[13px] text-slate-300"
186
+ >
187
+ {{ t('prReview.noFindings') }}
188
+ </div>
189
+
190
+ <template v-else>
191
+ <!-- Selection toolbar -->
192
+ <div v-if="awaiting" class="mb-2 flex items-center gap-3 text-[11px] text-slate-400">
193
+ <span data-testid="pr-review-selected-count">
194
+ {{ t('prReview.selectedCount', { count: selected.size }) }}
195
+ </span>
196
+ <button class="text-indigo-300 hover:underline" @click="selectAll">
197
+ {{ t('prReview.selectAll') }}
198
+ </button>
199
+ <button class="text-indigo-300 hover:underline" @click="clearAll">
200
+ {{ t('prReview.clear') }}
201
+ </button>
202
+ </div>
203
+
204
+ <!-- Findings grouped by slice -->
205
+ <section v-for="g in groups" :key="g.id" class="mb-4">
206
+ <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
207
+ {{ g.title }}
208
+ </h3>
209
+ <p v-if="g.rationale" class="mb-1.5 text-[11px] text-slate-500">
210
+ {{ g.rationale }}
211
+ </p>
212
+ <article
213
+ v-for="f in g.items"
214
+ :key="f.id"
215
+ data-testid="pr-review-finding"
216
+ class="mb-1.5 rounded-xl border px-3 py-2 transition"
217
+ :class="
218
+ awaiting && selected.has(f.id)
219
+ ? 'border-indigo-500/60 bg-indigo-500/5'
220
+ : 'border-slate-800 bg-slate-900/60'
221
+ "
222
+ >
223
+ <div class="flex items-start gap-2">
224
+ <input
225
+ v-if="awaiting"
226
+ type="checkbox"
227
+ class="mt-1 accent-indigo-500"
228
+ data-testid="pr-review-finding-toggle"
229
+ :checked="selected.has(f.id)"
230
+ @change="toggle(f.id)"
231
+ />
232
+ <div class="min-w-0 flex-1">
233
+ <div class="flex flex-wrap items-center gap-1.5">
234
+ <span
235
+ class="rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase ring-1"
236
+ :class="SEVERITY_CLASS[f.severity]"
237
+ >
238
+ {{ t(`prReview.severity.${f.severity}`) }}
239
+ </span>
240
+ <span class="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-300">
241
+ {{ t(`prReview.category.${f.category}`) }}
242
+ </span>
243
+ <h4 class="min-w-0 flex-1 text-[13px] font-medium text-slate-100">
244
+ {{ f.title }}
245
+ </h4>
246
+ </div>
247
+ <p class="mt-0.5 text-[11px] text-slate-500">
248
+ {{ f.path
249
+ }}<template v-if="f.line != null">
250
+ · {{ t('prReview.line', { line: f.line }) }}</template
251
+ >
252
+ </p>
253
+ <p class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
254
+ {{ f.detail }}
255
+ </p>
256
+ <p
257
+ v-if="f.suggestedFix"
258
+ class="mt-1 whitespace-pre-wrap rounded-md bg-slate-800/50 px-2 py-1 text-[11px] text-slate-300"
259
+ >
260
+ <span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
261
+ {{ f.suggestedFix }}
262
+ </p>
263
+ </div>
264
+ </div>
265
+ </article>
266
+ </section>
267
+ </template>
268
+ </template>
269
+ </div>
270
+
271
+ <!-- Footer -->
272
+ <footer
273
+ v-if="awaiting"
274
+ class="flex items-center justify-end gap-2 border-t border-slate-800 px-5 py-3"
275
+ >
276
+ <UButton
277
+ color="primary"
278
+ :loading="prReview.resolving"
279
+ :disabled="!canFinish"
280
+ data-testid="pr-review-finish"
281
+ @click="onFinish"
282
+ >
283
+ {{ t('prReview.finish') }}
284
+ </UButton>
285
+ </footer>
286
+ </div>
287
+ </div>
288
+ </Teleport>
289
+ </template>
@@ -39,6 +39,7 @@ const ROUTABLE = computed<{ type: NotificationType; label: string }[]>(() => [
39
39
  { type: 'release_regression', label: t('slack.routable.release_regression') },
40
40
  { type: 'human_test_ready', label: t('slack.routable.human_test_ready') },
41
41
  { type: 'visual_confirmation_ready', label: t('slack.routable.visual_confirmation_ready') },
42
+ { type: 'pr_review_ready', label: t('slack.routable.pr_review_ready') },
42
43
  { type: 'initiative', label: t('slack.routable.initiative') },
43
44
  ])
44
45
 
@@ -64,6 +65,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
64
65
  human_review: { enabled: false, channel: '' },
65
66
  followup_pending: { enabled: false, channel: '' },
66
67
  fork_decision_pending: { enabled: false, channel: '' },
68
+ pr_review_ready: { enabled: false, channel: '' },
67
69
  initiative: { enabled: false, channel: '' },
68
70
  })
69
71
  const mentionsEnabled = ref(false)
@@ -0,0 +1,29 @@
1
+ import { getPrReviewContract, resolvePrReviewContract } from '@cat-factory/contracts'
2
+ import type { ApiContext } from './context'
3
+
4
+ /**
5
+ * The PR deep-review phase: the read-only `pr-reviewer` agent slices an open pull request and
6
+ * surfaces prioritized findings on the run's `pr-reviewer` step, then the run parks for a human
7
+ * to SELECT which findings matter. These endpoints read the surfaced findings and record the
8
+ * human's curated selection + resolution. The read returns null when no `pr-reviewer` step
9
+ * carries review state.
10
+ */
11
+ export function prReviewApi({ send, ws }: ApiContext) {
12
+ return {
13
+ // The live PR-review state for a run (null when no pr-reviewer step carries one).
14
+ getPrReview: (workspaceId: string, executionId: string) =>
15
+ send(getPrReviewContract, { pathPrefix: ws(workspaceId), pathParams: { executionId } }),
16
+
17
+ // Resolve a parked PR review: the curated finding selection + how it was resolved.
18
+ resolvePrReview: (
19
+ workspaceId: string,
20
+ executionId: string,
21
+ body: { action?: 'finish'; findingIds?: string[] },
22
+ ) =>
23
+ send(resolvePrReviewContract, {
24
+ pathPrefix: ws(workspaceId),
25
+ pathParams: { executionId },
26
+ body,
27
+ }),
28
+ }
29
+ }
@@ -9,6 +9,7 @@ import { documentsApi } from './api/documents'
9
9
  import { executionApi } from './api/execution'
10
10
  import { followUpsApi } from './api/followUps'
11
11
  import { forkDecisionApi } from './api/forkDecision'
12
+ import { prReviewApi } from './api/prReview'
12
13
  import { fragmentsApi } from './api/fragments'
13
14
  import { githubApi } from './api/github'
14
15
  import { humanReviewApi } from './api/humanReview'
@@ -110,6 +111,7 @@ export function useApi() {
110
111
  ...reviewsApi(ctx),
111
112
  ...followUpsApi(ctx),
112
113
  ...forkDecisionApi(ctx),
114
+ ...prReviewApi(ctx),
113
115
  ...humanTestApi(ctx),
114
116
  ...visualConfirmApi(ctx),
115
117
  ...humanReviewApi(ctx),
@@ -0,0 +1,80 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { PrReviewStepState } from '~/types/execution'
4
+ import { useApi } from '~/composables/useApi'
5
+ import { useWorkspaceStore } from '~/stores/workspace'
6
+ import { useExecutionStore } from '~/stores/execution'
7
+
8
+ /**
9
+ * The PR deep-review action surface. The live review state lives on the run's `pr-reviewer`
10
+ * step (`step.prReview`) and is kept fresh by the execution stream, so the window reads it
11
+ * straight off the execution store — this store only wraps the `resolve` action (and a warm-up
12
+ * `load`), tracks the in-flight state so the window can disable its controls, and reflects the
13
+ * returned state back onto the execution store so the UI updates immediately even before the
14
+ * stream echoes the change. Keyed by executionId, mirroring the fork-decision store.
15
+ */
16
+ export const usePrReviewStore = defineStore('prReview', () => {
17
+ const api = useApi()
18
+ const workspace = useWorkspaceStore()
19
+ const execution = useExecutionStore()
20
+
21
+ /** True while a resolve call is in flight (drives the Finish button spinner / disabled state). */
22
+ const resolving = ref(false)
23
+ /** The last error message from an action, surfaced inline; cleared on the next action. */
24
+ const error = ref<string | null>(null)
25
+
26
+ /**
27
+ * Reflect an authoritative PR-review state onto the run's `pr-reviewer` step. A pipeline could
28
+ * carry more than one such step, so target the step this review is about: prefer the step that
29
+ * is still awaiting a selection, then the current step, and only then the first step carrying
30
+ * review state. The stream corrects any mismatch; this keeps the immediate optimistic echo on
31
+ * the right step.
32
+ */
33
+ function reflect(executionId: string, state: PrReviewStepState | null): void {
34
+ if (!state) return
35
+ const instance = execution.getInstance(executionId)
36
+ if (!instance) return
37
+ const isLive = (s: (typeof instance.steps)[number]) =>
38
+ s.agentKind === 'pr-reviewer' && s.prReview?.status === 'awaiting_selection'
39
+ const current = instance.steps[instance.currentStep]
40
+ const step =
41
+ instance.steps.find(isLive) ??
42
+ (current?.agentKind === 'pr-reviewer' && current.prReview ? current : undefined) ??
43
+ instance.steps.find((s) => s.prReview)
44
+ if (step) step.prReview = state
45
+ }
46
+
47
+ /** Warm the live state from the GET (the stream also keeps it fresh). Best-effort. */
48
+ async function load(executionId: string): Promise<void> {
49
+ error.value = null
50
+ try {
51
+ const state = await api.getPrReview(workspace.requireId(), executionId)
52
+ reflect(executionId, state as PrReviewStepState | null)
53
+ } catch (e) {
54
+ error.value = e instanceof Error ? e.message : 'Failed to load'
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Resolve the review: record the curated finding selection and complete the read-only review
60
+ * (the run then advances to done). PR 2 supports only the `finish` action.
61
+ */
62
+ async function resolve(executionId: string, findingIds: string[]): Promise<void> {
63
+ error.value = null
64
+ resolving.value = true
65
+ try {
66
+ const state = await api.resolvePrReview(workspace.requireId(), executionId, {
67
+ action: 'finish',
68
+ findingIds,
69
+ })
70
+ reflect(executionId, state as PrReviewStepState)
71
+ } catch (e) {
72
+ error.value = e instanceof Error ? e.message : 'Failed to resolve review'
73
+ throw e
74
+ } finally {
75
+ resolving.value = false
76
+ }
77
+ }
78
+
79
+ return { resolving, error, load, resolve }
80
+ })
package/app/stores/ui.ts CHANGED
@@ -834,6 +834,31 @@ export const useUiStore = defineStore('ui', () => {
834
834
  stepIndex: idx,
835
835
  }
836
836
  }
837
+ // Open the PR deep-review window for a run's `pr-reviewer` step (from the `pr_review_ready`
838
+ // notification / the step). Resolves the step index from the run when not given, preferring
839
+ // the step parked awaiting a finding selection.
840
+ function openPrReview(instanceId: string, stepIndex: number | null = null) {
841
+ const execution = useExecutionStore()
842
+ const instance = execution.getInstance(instanceId)
843
+ if (!instance) return
844
+ const resolveIdx = () => {
845
+ const awaiting = instance.steps.findIndex(
846
+ (s) => s.agentKind === 'pr-reviewer' && s.prReview?.status === 'awaiting_selection',
847
+ )
848
+ if (awaiting >= 0) return awaiting
849
+ const current = instance.steps[instance.currentStep]
850
+ if (current?.agentKind === 'pr-reviewer' && current.prReview) return instance.currentStep
851
+ return instance.steps.findIndex((s) => s.agentKind === 'pr-reviewer' && s.prReview)
852
+ }
853
+ const idx = stepIndex ?? resolveIdx()
854
+ if (idx < 0) return
855
+ resultView.value = {
856
+ view: 'pr-review',
857
+ blockId: instance.blockId,
858
+ instanceId,
859
+ stepIndex: idx,
860
+ }
861
+ }
837
862
  function closeResultView() {
838
863
  resultView.value = null
839
864
  }
@@ -1032,6 +1057,7 @@ export const useUiStore = defineStore('ui', () => {
1032
1057
  openInitiativePlanning,
1033
1058
  openFollowUps,
1034
1059
  openForkDecision,
1060
+ openPrReview,
1035
1061
  closeRequirementReview,
1036
1062
  openStepDetail,
1037
1063
  closeStepDetail,
@@ -38,6 +38,11 @@ export type {
38
38
  ForkDecisionStatus,
39
39
  ForkChoice,
40
40
  ForkDecisionStepState,
41
+ PrReviewStepState,
42
+ PrReviewFinding,
43
+ PrReviewSlice,
44
+ PrReviewSeverity,
45
+ PrReviewCategory,
41
46
  GateFailingCheck,
42
47
  GateAttempt,
43
48
  GateStepState,
@@ -1697,7 +1697,8 @@
1697
1697
  "followup_pending": "Als gelesen markieren",
1698
1698
  "initiative": "Als gelesen markieren",
1699
1699
  "markRead": "Als gelesen markieren",
1700
- "fork_decision_pending": "Als gelesen markieren"
1700
+ "fork_decision_pending": "Als gelesen markieren",
1701
+ "pr_review_ready": "Als gelesen markieren"
1701
1702
  }
1702
1703
  },
1703
1704
  "aiProvidersBanner": {
@@ -3923,6 +3924,7 @@
3923
3924
  "release_regression": "Release-Regression",
3924
3925
  "human_test_ready": "Bereit für manuelle Tests",
3925
3926
  "visual_confirmation_ready": "Bereit für visuelle Bestätigung",
3927
+ "pr_review_ready": "PR-Review-Befunde",
3926
3928
  "initiative": "Initiativen-Updates"
3927
3929
  },
3928
3930
  "role": {
@@ -4702,5 +4704,40 @@
4702
4704
  "empty": {
4703
4705
  "title": "Nichts zu entscheiden"
4704
4706
  }
4707
+ },
4708
+ "prReview": {
4709
+ "title": "PR-Review",
4710
+ "titleWithBlock": "PR-Review: {title}",
4711
+ "subtitle": "Wähle die Befunde aus, die bearbeitet werden sollen.",
4712
+ "openPr": "PR öffnen",
4713
+ "summaryLabel": "Zusammenfassung:",
4714
+ "noFindings": "Keine Befunde – der Pull Request sieht sauber aus.",
4715
+ "unsliced": "Sonstige",
4716
+ "selectAll": "Alle auswählen",
4717
+ "clear": "Zurücksetzen",
4718
+ "selectedCount": "{count} ausgewählt",
4719
+ "finish": "Review abschließen",
4720
+ "suggestedFix": "Lösungsvorschlag:",
4721
+ "line": "Zeile {line}",
4722
+ "reviewing": {
4723
+ "title": "Pull Request wird geprüft…",
4724
+ "hint": "Der Diff wird in zusammenhängende Teile zerlegt und einzeln geprüft."
4725
+ },
4726
+ "severity": {
4727
+ "blocker": "Blocker",
4728
+ "high": "Hoch",
4729
+ "medium": "Mittel",
4730
+ "low": "Niedrig",
4731
+ "nit": "Kleinigkeit"
4732
+ },
4733
+ "category": {
4734
+ "correctness": "Korrektheit",
4735
+ "security": "Sicherheit",
4736
+ "performance": "Performance",
4737
+ "maintainability": "Wartbarkeit",
4738
+ "style": "Stil",
4739
+ "test": "Tests",
4740
+ "other": "Sonstiges"
4741
+ }
4705
4742
  }
4706
4743
  }
@@ -1626,7 +1626,8 @@
1626
1626
  "followup_pending": "Mark read",
1627
1627
  "initiative": "Mark read",
1628
1628
  "markRead": "Mark read",
1629
- "fork_decision_pending": "Mark read"
1629
+ "fork_decision_pending": "Mark read",
1630
+ "pr_review_ready": "Mark read"
1630
1631
  }
1631
1632
  },
1632
1633
  "aiProvidersBanner": {
@@ -3072,6 +3073,7 @@
3072
3073
  "release_regression": "Release regression",
3073
3074
  "human_test_ready": "Ready for human testing",
3074
3075
  "visual_confirmation_ready": "Ready for visual confirmation",
3076
+ "pr_review_ready": "PR review findings",
3075
3077
  "initiative": "Initiative updates"
3076
3078
  },
3077
3079
  "role": {
@@ -4828,5 +4830,40 @@
4828
4830
  "empty": {
4829
4831
  "title": "Nothing to decide"
4830
4832
  }
4833
+ },
4834
+ "prReview": {
4835
+ "title": "PR review",
4836
+ "titleWithBlock": "PR review: {title}",
4837
+ "subtitle": "Select the findings to act on.",
4838
+ "openPr": "Open PR",
4839
+ "summaryLabel": "Summary:",
4840
+ "noFindings": "No findings — the pull request looks clean.",
4841
+ "unsliced": "Other",
4842
+ "selectAll": "Select all",
4843
+ "clear": "Clear",
4844
+ "selectedCount": "{count} selected",
4845
+ "finish": "Finish review",
4846
+ "suggestedFix": "Suggested fix:",
4847
+ "line": "line {line}",
4848
+ "reviewing": {
4849
+ "title": "Reviewing the pull request…",
4850
+ "hint": "Slicing the diff into cohesive chunks and reviewing each one."
4851
+ },
4852
+ "severity": {
4853
+ "blocker": "Blocker",
4854
+ "high": "High",
4855
+ "medium": "Medium",
4856
+ "low": "Low",
4857
+ "nit": "Nit"
4858
+ },
4859
+ "category": {
4860
+ "correctness": "Correctness",
4861
+ "security": "Security",
4862
+ "performance": "Performance",
4863
+ "maintainability": "Maintainability",
4864
+ "style": "Style",
4865
+ "test": "Tests",
4866
+ "other": "Other"
4867
+ }
4831
4868
  }
4832
4869
  }
@@ -1560,7 +1560,8 @@
1560
1560
  "followup_pending": "Marcar como leída",
1561
1561
  "initiative": "Marcar como leída",
1562
1562
  "markRead": "Marcar como leída",
1563
- "fork_decision_pending": "Marcar como leído"
1563
+ "fork_decision_pending": "Marcar como leído",
1564
+ "pr_review_ready": "Marcar como leída"
1564
1565
  },
1565
1566
  "toast": {
1566
1567
  "acted": "Marcado como resuelto",
@@ -2979,6 +2980,7 @@
2979
2980
  "release_regression": "Regresion de version",
2980
2981
  "human_test_ready": "Listo para pruebas humanas",
2981
2982
  "visual_confirmation_ready": "Listo para confirmacion visual",
2983
+ "pr_review_ready": "Hallazgos de revisión de PR",
2982
2984
  "initiative": "Actualizaciones de la iniciativa"
2983
2985
  },
2984
2986
  "role": {
@@ -4690,5 +4692,40 @@
4690
4692
  "empty": {
4691
4693
  "title": "Nada que decidir"
4692
4694
  }
4695
+ },
4696
+ "prReview": {
4697
+ "title": "Revisión de PR",
4698
+ "titleWithBlock": "Revisión de PR: {title}",
4699
+ "subtitle": "Selecciona los hallazgos sobre los que actuar.",
4700
+ "openPr": "Abrir PR",
4701
+ "summaryLabel": "Resumen:",
4702
+ "noFindings": "Sin hallazgos: el pull request parece correcto.",
4703
+ "unsliced": "Otros",
4704
+ "selectAll": "Seleccionar todo",
4705
+ "clear": "Limpiar",
4706
+ "selectedCount": "{count} seleccionados",
4707
+ "finish": "Finalizar revisión",
4708
+ "suggestedFix": "Corrección sugerida:",
4709
+ "line": "línea {line}",
4710
+ "reviewing": {
4711
+ "title": "Revisando el pull request…",
4712
+ "hint": "Dividiendo el diff en bloques coherentes y revisando cada uno."
4713
+ },
4714
+ "severity": {
4715
+ "blocker": "Bloqueante",
4716
+ "high": "Alta",
4717
+ "medium": "Media",
4718
+ "low": "Baja",
4719
+ "nit": "Menor"
4720
+ },
4721
+ "category": {
4722
+ "correctness": "Corrección",
4723
+ "security": "Seguridad",
4724
+ "performance": "Rendimiento",
4725
+ "maintainability": "Mantenibilidad",
4726
+ "style": "Estilo",
4727
+ "test": "Pruebas",
4728
+ "other": "Otros"
4729
+ }
4693
4730
  }
4694
4731
  }
@@ -1560,7 +1560,8 @@
1560
1560
  "followup_pending": "Marquer comme lu",
1561
1561
  "initiative": "Marquer comme lu",
1562
1562
  "markRead": "Marquer comme lu",
1563
- "fork_decision_pending": "Marquer comme lu"
1563
+ "fork_decision_pending": "Marquer comme lu",
1564
+ "pr_review_ready": "Marquer comme lu"
1564
1565
  },
1565
1566
  "toast": {
1566
1567
  "acted": "Marqué comme traité",
@@ -2979,6 +2980,7 @@
2979
2980
  "release_regression": "Regression de version",
2980
2981
  "human_test_ready": "Pret pour les tests humains",
2981
2982
  "visual_confirmation_ready": "Pret pour la confirmation visuelle",
2983
+ "pr_review_ready": "Points de revue de PR",
2982
2984
  "initiative": "Mises a jour de l'initiative"
2983
2985
  },
2984
2986
  "role": {
@@ -4690,5 +4692,40 @@
4690
4692
  "empty": {
4691
4693
  "title": "Rien à décider"
4692
4694
  }
4695
+ },
4696
+ "prReview": {
4697
+ "title": "Revue de PR",
4698
+ "titleWithBlock": "Revue de PR : {title}",
4699
+ "subtitle": "Sélectionnez les points à traiter.",
4700
+ "openPr": "Ouvrir la PR",
4701
+ "summaryLabel": "Résumé :",
4702
+ "noFindings": "Aucun point relevé — la pull request semble propre.",
4703
+ "unsliced": "Autres",
4704
+ "selectAll": "Tout sélectionner",
4705
+ "clear": "Effacer",
4706
+ "selectedCount": "{count} sélectionné(s)",
4707
+ "finish": "Terminer la revue",
4708
+ "suggestedFix": "Correction suggérée :",
4709
+ "line": "ligne {line}",
4710
+ "reviewing": {
4711
+ "title": "Revue de la pull request…",
4712
+ "hint": "Découpage du diff en blocs cohérents et revue de chacun."
4713
+ },
4714
+ "severity": {
4715
+ "blocker": "Bloquant",
4716
+ "high": "Élevée",
4717
+ "medium": "Moyenne",
4718
+ "low": "Faible",
4719
+ "nit": "Mineur"
4720
+ },
4721
+ "category": {
4722
+ "correctness": "Exactitude",
4723
+ "security": "Sécurité",
4724
+ "performance": "Performance",
4725
+ "maintainability": "Maintenabilité",
4726
+ "style": "Style",
4727
+ "test": "Tests",
4728
+ "other": "Autre"
4729
+ }
4693
4730
  }
4694
4731
  }
@@ -1560,7 +1560,8 @@
1560
1560
  "followup_pending": "סמן כנקרא",
1561
1561
  "initiative": "סמן כנקרא",
1562
1562
  "markRead": "סמן כנקרא",
1563
- "fork_decision_pending": "סמן כנקרא"
1563
+ "fork_decision_pending": "סמן כנקרא",
1564
+ "pr_review_ready": "סמן כנקרא"
1564
1565
  },
1565
1566
  "toast": {
1566
1567
  "acted": "סומן כטופל",
@@ -2990,6 +2991,7 @@
2990
2991
  "release_regression": "רגרסיית שחרור",
2991
2992
  "human_test_ready": "מוכן לבדיקה אנושית",
2992
2993
  "visual_confirmation_ready": "מוכן לאישור חזותי",
2994
+ "pr_review_ready": "ממצאי בדיקת PR",
2993
2995
  "initiative": "עדכוני יוזמה"
2994
2996
  },
2995
2997
  "role": {
@@ -4701,5 +4703,40 @@
4701
4703
  "empty": {
4702
4704
  "title": "אין מה להחליט"
4703
4705
  }
4706
+ },
4707
+ "prReview": {
4708
+ "title": "בדיקת PR",
4709
+ "titleWithBlock": "בדיקת PR: {title}",
4710
+ "subtitle": "בחר את הממצאים לטיפול.",
4711
+ "openPr": "פתח PR",
4712
+ "summaryLabel": "סיכום:",
4713
+ "noFindings": "אין ממצאים — בקשת המשיכה נראית תקינה.",
4714
+ "unsliced": "אחר",
4715
+ "selectAll": "בחר הכול",
4716
+ "clear": "נקה",
4717
+ "selectedCount": "{count} נבחרו",
4718
+ "finish": "סיים בדיקה",
4719
+ "suggestedFix": "תיקון מוצע:",
4720
+ "line": "שורה {line}",
4721
+ "reviewing": {
4722
+ "title": "בודק את בקשת המשיכה…",
4723
+ "hint": "מחלק את ההבדלים לקטעים לכידים ובודק כל אחד."
4724
+ },
4725
+ "severity": {
4726
+ "blocker": "חוסם",
4727
+ "high": "גבוה",
4728
+ "medium": "בינוני",
4729
+ "low": "נמוך",
4730
+ "nit": "זניח"
4731
+ },
4732
+ "category": {
4733
+ "correctness": "נכונות",
4734
+ "security": "אבטחה",
4735
+ "performance": "ביצועים",
4736
+ "maintainability": "תחזוקתיות",
4737
+ "style": "סגנון",
4738
+ "test": "בדיקות",
4739
+ "other": "אחר"
4740
+ }
4704
4741
  }
4705
4742
  }
@@ -1697,7 +1697,8 @@
1697
1697
  "followup_pending": "Segna come letto",
1698
1698
  "initiative": "Segna come letto",
1699
1699
  "markRead": "Segna come letto",
1700
- "fork_decision_pending": "Segna come letto"
1700
+ "fork_decision_pending": "Segna come letto",
1701
+ "pr_review_ready": "Segna come letto"
1701
1702
  }
1702
1703
  },
1703
1704
  "aiProvidersBanner": {
@@ -3923,6 +3924,7 @@
3923
3924
  "release_regression": "Regressione della release",
3924
3925
  "human_test_ready": "Pronto per il test umano",
3925
3926
  "visual_confirmation_ready": "Pronto per la conferma visiva",
3927
+ "pr_review_ready": "Rilievi revisione PR",
3926
3928
  "initiative": "Aggiornamenti dell'iniziativa"
3927
3929
  },
3928
3930
  "role": {
@@ -4702,5 +4704,40 @@
4702
4704
  "empty": {
4703
4705
  "title": "Nulla da decidere"
4704
4706
  }
4707
+ },
4708
+ "prReview": {
4709
+ "title": "Revisione PR",
4710
+ "titleWithBlock": "Revisione PR: {title}",
4711
+ "subtitle": "Seleziona i rilievi su cui intervenire.",
4712
+ "openPr": "Apri PR",
4713
+ "summaryLabel": "Riepilogo:",
4714
+ "noFindings": "Nessun rilievo — la pull request sembra pulita.",
4715
+ "unsliced": "Altro",
4716
+ "selectAll": "Seleziona tutto",
4717
+ "clear": "Cancella",
4718
+ "selectedCount": "{count} selezionati",
4719
+ "finish": "Concludi revisione",
4720
+ "suggestedFix": "Correzione suggerita:",
4721
+ "line": "riga {line}",
4722
+ "reviewing": {
4723
+ "title": "Revisione della pull request…",
4724
+ "hint": "Suddivisione del diff in blocchi coerenti e revisione di ciascuno."
4725
+ },
4726
+ "severity": {
4727
+ "blocker": "Bloccante",
4728
+ "high": "Alta",
4729
+ "medium": "Media",
4730
+ "low": "Bassa",
4731
+ "nit": "Minore"
4732
+ },
4733
+ "category": {
4734
+ "correctness": "Correttezza",
4735
+ "security": "Sicurezza",
4736
+ "performance": "Prestazioni",
4737
+ "maintainability": "Manutenibilità",
4738
+ "style": "Stile",
4739
+ "test": "Test",
4740
+ "other": "Altro"
4741
+ }
4705
4742
  }
4706
4743
  }
@@ -1560,7 +1560,8 @@
1560
1560
  "followup_pending": "既読にする",
1561
1561
  "initiative": "既読にする",
1562
1562
  "markRead": "既読にする",
1563
- "fork_decision_pending": "既読にする"
1563
+ "fork_decision_pending": "既読にする",
1564
+ "pr_review_ready": "既読にする"
1564
1565
  },
1565
1566
  "toast": {
1566
1567
  "acted": "対応済みにしました",
@@ -2991,6 +2992,7 @@
2991
2992
  "release_regression": "リリースリグレッション",
2992
2993
  "human_test_ready": "人手テスト準備完了",
2993
2994
  "visual_confirmation_ready": "ビジュアル確認準備完了",
2995
+ "pr_review_ready": "PRレビューの指摘",
2994
2996
  "initiative": "イニシアチブの更新"
2995
2997
  },
2996
2998
  "role": {
@@ -4702,5 +4704,40 @@
4702
4704
  "empty": {
4703
4705
  "title": "決定する項目はありません"
4704
4706
  }
4707
+ },
4708
+ "prReview": {
4709
+ "title": "PRレビュー",
4710
+ "titleWithBlock": "PRレビュー: {title}",
4711
+ "subtitle": "対応する指摘を選択してください。",
4712
+ "openPr": "PRを開く",
4713
+ "summaryLabel": "概要:",
4714
+ "noFindings": "指摘はありません。プルリクエストは問題なさそうです。",
4715
+ "unsliced": "その他",
4716
+ "selectAll": "すべて選択",
4717
+ "clear": "クリア",
4718
+ "selectedCount": "{count}件選択中",
4719
+ "finish": "レビューを完了",
4720
+ "suggestedFix": "修正案:",
4721
+ "line": "{line}行目",
4722
+ "reviewing": {
4723
+ "title": "プルリクエストをレビュー中…",
4724
+ "hint": "差分をまとまりのある単位に分割し、各単位をレビューしています。"
4725
+ },
4726
+ "severity": {
4727
+ "blocker": "ブロッカー",
4728
+ "high": "高",
4729
+ "medium": "中",
4730
+ "low": "低",
4731
+ "nit": "軽微"
4732
+ },
4733
+ "category": {
4734
+ "correctness": "正確性",
4735
+ "security": "セキュリティ",
4736
+ "performance": "パフォーマンス",
4737
+ "maintainability": "保守性",
4738
+ "style": "スタイル",
4739
+ "test": "テスト",
4740
+ "other": "その他"
4741
+ }
4705
4742
  }
4706
4743
  }
@@ -1560,7 +1560,8 @@
1560
1560
  "followup_pending": "Oznacz jako przeczytane",
1561
1561
  "initiative": "Oznacz jako przeczytane",
1562
1562
  "markRead": "Oznacz jako przeczytane",
1563
- "fork_decision_pending": "Oznacz jako przeczytane"
1563
+ "fork_decision_pending": "Oznacz jako przeczytane",
1564
+ "pr_review_ready": "Oznacz jako przeczytane"
1564
1565
  },
1565
1566
  "toast": {
1566
1567
  "acted": "Oznaczono jako obsłużone",
@@ -2979,6 +2980,7 @@
2979
2980
  "release_regression": "Regresja wydania",
2980
2981
  "human_test_ready": "Gotowe do testow przez czlowieka",
2981
2982
  "visual_confirmation_ready": "Gotowe do potwierdzenia wizualnego",
2983
+ "pr_review_ready": "Uwagi z przeglądu PR",
2982
2984
  "initiative": "Aktualizacje inicjatywy"
2983
2985
  },
2984
2986
  "role": {
@@ -4690,5 +4692,40 @@
4690
4692
  "empty": {
4691
4693
  "title": "Nie ma czego decydować"
4692
4694
  }
4695
+ },
4696
+ "prReview": {
4697
+ "title": "Przegląd PR",
4698
+ "titleWithBlock": "Przegląd PR: {title}",
4699
+ "subtitle": "Wybierz uwagi, którymi chcesz się zająć.",
4700
+ "openPr": "Otwórz PR",
4701
+ "summaryLabel": "Podsumowanie:",
4702
+ "noFindings": "Brak uwag — pull request wygląda dobrze.",
4703
+ "unsliced": "Inne",
4704
+ "selectAll": "Zaznacz wszystko",
4705
+ "clear": "Wyczyść",
4706
+ "selectedCount": "Wybrano: {count}",
4707
+ "finish": "Zakończ przegląd",
4708
+ "suggestedFix": "Sugerowana poprawka:",
4709
+ "line": "wiersz {line}",
4710
+ "reviewing": {
4711
+ "title": "Przeglądanie pull requesta…",
4712
+ "hint": "Dzielenie zmian na spójne części i przeglądanie każdej z nich."
4713
+ },
4714
+ "severity": {
4715
+ "blocker": "Blokujące",
4716
+ "high": "Wysokie",
4717
+ "medium": "Średnie",
4718
+ "low": "Niskie",
4719
+ "nit": "Drobiazg"
4720
+ },
4721
+ "category": {
4722
+ "correctness": "Poprawność",
4723
+ "security": "Bezpieczeństwo",
4724
+ "performance": "Wydajność",
4725
+ "maintainability": "Utrzymywalność",
4726
+ "style": "Styl",
4727
+ "test": "Testy",
4728
+ "other": "Inne"
4729
+ }
4693
4730
  }
4694
4731
  }
@@ -1560,7 +1560,8 @@
1560
1560
  "followup_pending": "Okundu işaretle",
1561
1561
  "initiative": "Okundu işaretle",
1562
1562
  "markRead": "Okundu işaretle",
1563
- "fork_decision_pending": "Okundu olarak işaretle"
1563
+ "fork_decision_pending": "Okundu olarak işaretle",
1564
+ "pr_review_ready": "Okundu işaretle"
1564
1565
  },
1565
1566
  "toast": {
1566
1567
  "acted": "İşlendi olarak işaretlendi",
@@ -2991,6 +2992,7 @@
2991
2992
  "release_regression": "Sürüm gerilemesi",
2992
2993
  "human_test_ready": "İnsan testine hazır",
2993
2994
  "visual_confirmation_ready": "Görsel onaya hazır",
2995
+ "pr_review_ready": "PR inceleme bulguları",
2994
2996
  "initiative": "Girisim guncellemeleri"
2995
2997
  },
2996
2998
  "role": {
@@ -4702,5 +4704,40 @@
4702
4704
  "empty": {
4703
4705
  "title": "Karar verilecek bir şey yok"
4704
4706
  }
4707
+ },
4708
+ "prReview": {
4709
+ "title": "PR incelemesi",
4710
+ "titleWithBlock": "PR incelemesi: {title}",
4711
+ "subtitle": "İşlem yapılacak bulguları seçin.",
4712
+ "openPr": "PR'ı aç",
4713
+ "summaryLabel": "Özet:",
4714
+ "noFindings": "Bulgu yok — pull request temiz görünüyor.",
4715
+ "unsliced": "Diğer",
4716
+ "selectAll": "Tümünü seç",
4717
+ "clear": "Temizle",
4718
+ "selectedCount": "{count} seçildi",
4719
+ "finish": "İncelemeyi bitir",
4720
+ "suggestedFix": "Önerilen düzeltme:",
4721
+ "line": "satır {line}",
4722
+ "reviewing": {
4723
+ "title": "Pull request inceleniyor…",
4724
+ "hint": "Fark tutarlı parçalara bölünüp her biri inceleniyor."
4725
+ },
4726
+ "severity": {
4727
+ "blocker": "Engelleyici",
4728
+ "high": "Yüksek",
4729
+ "medium": "Orta",
4730
+ "low": "Düşük",
4731
+ "nit": "Küçük"
4732
+ },
4733
+ "category": {
4734
+ "correctness": "Doğruluk",
4735
+ "security": "Güvenlik",
4736
+ "performance": "Performans",
4737
+ "maintainability": "Bakım kolaylığı",
4738
+ "style": "Stil",
4739
+ "test": "Testler",
4740
+ "other": "Diğer"
4741
+ }
4705
4742
  }
4706
4743
  }
@@ -1560,7 +1560,8 @@
1560
1560
  "followup_pending": "Позначити прочитаним",
1561
1561
  "initiative": "Позначити прочитаним",
1562
1562
  "markRead": "Позначити прочитаним",
1563
- "fork_decision_pending": "Позначити прочитаним"
1563
+ "fork_decision_pending": "Позначити прочитаним",
1564
+ "pr_review_ready": "Позначити прочитаним"
1564
1565
  },
1565
1566
  "toast": {
1566
1567
  "acted": "Позначено як опрацьоване",
@@ -2979,6 +2980,7 @@
2979
2980
  "release_regression": "Регресія випуску",
2980
2981
  "human_test_ready": "Готово до тестування людиною",
2981
2982
  "visual_confirmation_ready": "Готово до візуального підтвердження",
2983
+ "pr_review_ready": "Зауваження огляду PR",
2982
2984
  "initiative": "Оновлення ініціативи"
2983
2985
  },
2984
2986
  "role": {
@@ -4690,5 +4692,40 @@
4690
4692
  "empty": {
4691
4693
  "title": "Нема чого вирішувати"
4692
4694
  }
4695
+ },
4696
+ "prReview": {
4697
+ "title": "Огляд PR",
4698
+ "titleWithBlock": "Огляд PR: {title}",
4699
+ "subtitle": "Виберіть зауваження для опрацювання.",
4700
+ "openPr": "Відкрити PR",
4701
+ "summaryLabel": "Підсумок:",
4702
+ "noFindings": "Зауважень немає — pull request виглядає чистим.",
4703
+ "unsliced": "Інше",
4704
+ "selectAll": "Вибрати все",
4705
+ "clear": "Очистити",
4706
+ "selectedCount": "Вибрано: {count}",
4707
+ "finish": "Завершити огляд",
4708
+ "suggestedFix": "Пропоноване виправлення:",
4709
+ "line": "рядок {line}",
4710
+ "reviewing": {
4711
+ "title": "Перевірка pull request…",
4712
+ "hint": "Поділ змін на цілісні частини та огляд кожної з них."
4713
+ },
4714
+ "severity": {
4715
+ "blocker": "Блокер",
4716
+ "high": "Високий",
4717
+ "medium": "Середній",
4718
+ "low": "Низький",
4719
+ "nit": "Дрібниця"
4720
+ },
4721
+ "category": {
4722
+ "correctness": "Коректність",
4723
+ "security": "Безпека",
4724
+ "performance": "Продуктивність",
4725
+ "maintainability": "Підтримуваність",
4726
+ "style": "Стиль",
4727
+ "test": "Тести",
4728
+ "other": "Інше"
4729
+ }
4693
4730
  }
4694
4731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.117.0",
3
+ "version": "0.118.0",
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",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.130.0"
37
+ "@cat-factory/contracts": "0.131.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",