@cat-factory/app 0.138.1 → 0.139.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.
@@ -18,11 +18,9 @@ const agents = useAgentsStore()
18
18
  const { t } = useI18n()
19
19
 
20
20
  // Shared seam contract (open/blockId/close). No `onOpen` loader: this window reads its data
21
- // straight off the execution step, so there's nothing to fetch on open. `manageEscape: false`
22
- // — `ResultWindowShell` owns Escape (and focus trap + scroll lock + stacking).
23
- const { open, blockId, instanceId, stepIndex, close } = useResultView('generic-structured', {
24
- manageEscape: false,
25
- })
21
+ // straight off the execution step, so there's nothing to fetch on open. `ResultWindowShell`
22
+ // owns Escape (and focus trap + scroll lock + stacking).
23
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('generic-structured')
26
24
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
27
25
 
28
26
  const instance = computed(() =>
@@ -18,11 +18,9 @@ const agents = useAgentsStore()
18
18
  const { t, n } = useI18n()
19
19
 
20
20
  // Shared seam contract (open/blockId/close). No loader: the verdict is read straight off
21
- // the execution step. `manageEscape: false` — `ResultWindowShell` owns Escape (and focus
22
- // trap + scroll lock + stacking) via the shared overlay behaviour.
23
- const { open, blockId, instanceId, stepIndex, close } = useResultView('merger', {
24
- manageEscape: false,
25
- })
21
+ // the execution step. `ResultWindowShell` owns Escape (and focus trap + scroll lock +
22
+ // stacking) via the shared overlay behaviour.
23
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('merger')
26
24
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
27
25
 
28
26
  const instance = computed(() =>
@@ -12,7 +12,7 @@
12
12
  // `useModalBehavior` (`@modular-vue/core`, the slice-5 overlay-host release): focus-trap
13
13
  // + focus-return, body-scroll lock, and a shared overlay STACK so the top overlay closes
14
14
  // first on Escape. A window becomes body-only markup wrapped in `<ResultWindowShell>`; it
15
- // keeps its `useResultView` seam but passes `manageEscape: false` (the shell owns Escape).
15
+ // keeps its `useResultView` seam (Escape lives in this shell now, not in `useResultView`).
16
16
  //
17
17
  // The pick-one SELECTION of which window is active stays exactly the slice-2
18
18
  // `resolveComponentRegistry` in `StepResultViewHost.vue` — this shell only owns the
@@ -18,6 +18,7 @@ import type {
18
18
  PrReviewSeverity,
19
19
  PrReviewStepState,
20
20
  } from '~/types/execution'
21
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
21
22
 
22
23
  const execution = useExecutionStore()
23
24
  const board = useBoardStore()
@@ -122,219 +123,194 @@ async function onResolve(action: PrReviewResolution): Promise<void> {
122
123
  </script>
123
124
 
124
125
  <template>
125
- <Teleport to="body">
126
- <div
127
- v-if="open"
128
- data-testid="pr-review-window"
129
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
130
- @click.self="close"
131
- >
132
- <div
133
- class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
134
- role="dialog"
135
- aria-modal="true"
126
+ <ResultWindowShell
127
+ :open="open"
128
+ icon="i-lucide-clipboard-check"
129
+ icon-class="bg-indigo-500/15 text-indigo-300"
130
+ :title="block ? t('prReview.titleWithBlock', { title: block.title }) : t('prReview.title')"
131
+ :subtitle="t('prReview.subtitle')"
132
+ width="3xl"
133
+ testid="pr-review-window"
134
+ @close="close"
135
+ >
136
+ <template v-if="state?.prUrl" #header-extras>
137
+ <a
138
+ :href="state.prUrl"
139
+ target="_blank"
140
+ rel="noopener"
141
+ class="rounded-md px-2 py-1 text-[11px] text-indigo-300 hover:bg-slate-800"
136
142
  >
137
- <!-- Header -->
138
- <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
139
- <span
140
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-300"
141
- >
142
- <UIcon name="i-lucide-clipboard-check" class="h-4 w-4" />
143
- </span>
144
- <div class="min-w-0 flex-1">
145
- <h2 class="truncate text-sm font-semibold text-slate-100">
146
- {{
147
- block ? t('prReview.titleWithBlock', { title: block.title }) : t('prReview.title')
148
- }}
149
- </h2>
150
- <p class="truncate text-[11px] text-slate-400">{{ t('prReview.subtitle') }}</p>
151
- </div>
152
- <a
153
- v-if="state?.prUrl"
154
- :href="state.prUrl"
155
- target="_blank"
156
- rel="noopener"
157
- class="rounded-md px-2 py-1 text-[11px] text-indigo-300 hover:bg-slate-800"
158
- >
159
- {{ t('prReview.openPr') }}
160
- </a>
161
- <button
162
- class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
163
- @click="close"
164
- >
165
- <UIcon name="i-lucide-x" class="h-4 w-4" />
166
- </button>
167
- </header>
143
+ {{ t('prReview.openPr') }}
144
+ </a>
145
+ </template>
168
146
 
169
- <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
170
- <!-- Reviewing: the read-only reviewer is still working. -->
171
- <div
172
- v-if="status === 'reviewing'"
173
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
174
- >
175
- <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
176
- <p class="text-sm">{{ t('prReview.reviewing.title') }}</p>
177
- <p class="max-w-sm text-[11px] text-slate-500">{{ t('prReview.reviewing.hint') }}</p>
178
- </div>
147
+ <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
148
+ <!-- Reviewing: the read-only reviewer is still working. -->
149
+ <div
150
+ v-if="status === 'reviewing'"
151
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
152
+ >
153
+ <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
154
+ <p class="text-sm">{{ t('prReview.reviewing.title') }}</p>
155
+ <p class="max-w-sm text-[11px] text-slate-500">{{ t('prReview.reviewing.hint') }}</p>
156
+ </div>
179
157
 
180
- <!-- A resolution is executing: the Fixer is committing / comments are being posted. -->
181
- <div
182
- v-else-if="working"
183
- data-testid="pr-review-working"
184
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
185
- >
186
- <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
187
- <p class="text-sm">
188
- {{ status === 'fixing' ? t('prReview.fixing.title') : t('prReview.posting.title') }}
189
- </p>
190
- <p class="max-w-sm text-[11px] text-slate-500">
191
- {{ status === 'fixing' ? t('prReview.fixing.hint') : t('prReview.posting.hint') }}
192
- </p>
193
- </div>
158
+ <!-- A resolution is executing: the Fixer is committing / comments are being posted. -->
159
+ <div
160
+ v-else-if="working"
161
+ data-testid="pr-review-working"
162
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
163
+ >
164
+ <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
165
+ <p class="text-sm">
166
+ {{ status === 'fixing' ? t('prReview.fixing.title') : t('prReview.posting.title') }}
167
+ </p>
168
+ <p class="max-w-sm text-[11px] text-slate-500">
169
+ {{ status === 'fixing' ? t('prReview.fixing.hint') : t('prReview.posting.hint') }}
170
+ </p>
171
+ </div>
194
172
 
195
- <template v-else>
196
- <p
197
- v-if="prReview.error"
198
- class="mb-3 rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
199
- >
200
- {{ prReview.error }}
201
- </p>
173
+ <template v-else>
174
+ <p
175
+ v-if="prReview.error"
176
+ class="mb-3 rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
177
+ >
178
+ {{ prReview.error }}
179
+ </p>
202
180
 
203
- <!-- The reviewer's overall assessment. -->
204
- <p
205
- v-if="state?.summary"
206
- class="mb-3 rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
207
- >
208
- <span class="text-slate-500">{{ t('prReview.summaryLabel') }}</span>
209
- {{ state.summary }}
210
- </p>
181
+ <!-- The reviewer's overall assessment. -->
182
+ <p
183
+ v-if="state?.summary"
184
+ class="mb-3 rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
185
+ >
186
+ <span class="text-slate-500">{{ t('prReview.summaryLabel') }}</span>
187
+ {{ state.summary }}
188
+ </p>
211
189
 
212
- <!-- A clean PR / resolved review with no findings. -->
213
- <div
214
- v-if="findings.length === 0"
215
- class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-6 text-center text-[13px] text-slate-300"
216
- >
217
- {{ t('prReview.noFindings') }}
218
- </div>
190
+ <!-- A clean PR / resolved review with no findings. -->
191
+ <div
192
+ v-if="findings.length === 0"
193
+ class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-6 text-center text-[13px] text-slate-300"
194
+ >
195
+ {{ t('prReview.noFindings') }}
196
+ </div>
219
197
 
220
- <template v-else>
221
- <!-- Selection toolbar -->
222
- <div v-if="awaiting" class="mb-2 flex items-center gap-3 text-[11px] text-slate-400">
223
- <span data-testid="pr-review-selected-count">
224
- {{ t('prReview.selectedCount', { count: selected.size }) }}
225
- </span>
226
- <button class="text-indigo-300 hover:underline" @click="selectAll">
227
- {{ t('prReview.selectAll') }}
228
- </button>
229
- <button class="text-indigo-300 hover:underline" @click="clearAll">
230
- {{ t('prReview.clear') }}
231
- </button>
232
- </div>
198
+ <template v-else>
199
+ <!-- Selection toolbar -->
200
+ <div v-if="awaiting" class="mb-2 flex items-center gap-3 text-[11px] text-slate-400">
201
+ <span data-testid="pr-review-selected-count">
202
+ {{ t('prReview.selectedCount', { count: selected.size }) }}
203
+ </span>
204
+ <button class="text-indigo-300 hover:underline" @click="selectAll">
205
+ {{ t('prReview.selectAll') }}
206
+ </button>
207
+ <button class="text-indigo-300 hover:underline" @click="clearAll">
208
+ {{ t('prReview.clear') }}
209
+ </button>
210
+ </div>
233
211
 
234
- <!-- Findings grouped by slice -->
235
- <section v-for="g in groups" :key="g.id" class="mb-4">
236
- <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
237
- {{ g.title }}
238
- </h3>
239
- <p v-if="g.rationale" class="mb-1.5 text-[11px] text-slate-500">
240
- {{ g.rationale }}
241
- </p>
242
- <article
243
- v-for="f in g.items"
244
- :key="f.id"
245
- data-testid="pr-review-finding"
246
- class="mb-1.5 rounded-xl border px-3 py-2 transition"
247
- :class="
248
- awaiting && selected.has(f.id)
249
- ? 'border-indigo-500/60 bg-indigo-500/5'
250
- : 'border-slate-800 bg-slate-900/60'
251
- "
252
- >
253
- <div class="flex items-start gap-2">
254
- <input
255
- v-if="awaiting"
256
- type="checkbox"
257
- class="mt-1 accent-indigo-500"
258
- data-testid="pr-review-finding-toggle"
259
- :checked="selected.has(f.id)"
260
- @change="toggle(f.id)"
261
- />
262
- <div class="min-w-0 flex-1">
263
- <div class="flex flex-wrap items-center gap-1.5">
264
- <span
265
- class="rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase ring-1"
266
- :class="SEVERITY_CLASS[f.severity]"
267
- >
268
- {{ t(`prReview.severity.${f.severity}`) }}
269
- </span>
270
- <span class="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-300">
271
- {{ t(`prReview.category.${f.category}`) }}
272
- </span>
273
- <h4 class="min-w-0 flex-1 text-[13px] font-medium text-slate-100">
274
- {{ f.title }}
275
- </h4>
276
- </div>
277
- <p class="mt-0.5 text-[11px] text-slate-500">
278
- {{ f.path
279
- }}<template v-if="f.line != null">
280
- · {{ t('prReview.line', { line: f.line }) }}</template
281
- >
282
- </p>
283
- <p class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
284
- {{ f.detail }}
285
- </p>
286
- <p
287
- v-if="f.suggestedFix"
288
- class="mt-1 whitespace-pre-wrap rounded-md bg-slate-800/50 px-2 py-1 text-[11px] text-slate-300"
289
- >
290
- <span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
291
- {{ f.suggestedFix }}
292
- </p>
293
- </div>
212
+ <!-- Findings grouped by slice -->
213
+ <section v-for="g in groups" :key="g.id" class="mb-4">
214
+ <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
215
+ {{ g.title }}
216
+ </h3>
217
+ <p v-if="g.rationale" class="mb-1.5 text-[11px] text-slate-500">
218
+ {{ g.rationale }}
219
+ </p>
220
+ <article
221
+ v-for="f in g.items"
222
+ :key="f.id"
223
+ data-testid="pr-review-finding"
224
+ class="mb-1.5 rounded-xl border px-3 py-2 transition"
225
+ :class="
226
+ awaiting && selected.has(f.id)
227
+ ? 'border-indigo-500/60 bg-indigo-500/5'
228
+ : 'border-slate-800 bg-slate-900/60'
229
+ "
230
+ >
231
+ <div class="flex items-start gap-2">
232
+ <input
233
+ v-if="awaiting"
234
+ type="checkbox"
235
+ class="mt-1 accent-indigo-500"
236
+ data-testid="pr-review-finding-toggle"
237
+ :checked="selected.has(f.id)"
238
+ @change="toggle(f.id)"
239
+ />
240
+ <div class="min-w-0 flex-1">
241
+ <div class="flex flex-wrap items-center gap-1.5">
242
+ <span
243
+ class="rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase ring-1"
244
+ :class="SEVERITY_CLASS[f.severity]"
245
+ >
246
+ {{ t(`prReview.severity.${f.severity}`) }}
247
+ </span>
248
+ <span class="rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-300">
249
+ {{ t(`prReview.category.${f.category}`) }}
250
+ </span>
251
+ <h4 class="min-w-0 flex-1 text-[13px] font-medium text-slate-100">
252
+ {{ f.title }}
253
+ </h4>
294
254
  </div>
295
- </article>
296
- </section>
297
- </template>
298
- </template>
299
- </div>
300
-
301
- <!-- Footer -->
302
- <footer
303
- v-if="awaiting"
304
- class="flex items-center justify-end gap-2 border-t border-slate-800 px-5 py-3"
305
- >
306
- <UButton
307
- color="neutral"
308
- variant="ghost"
309
- :disabled="!canResolve || !access.canExecuteRuns.value"
310
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
311
- data-testid="pr-review-finish"
312
- @click="onResolve('finish')"
313
- >
314
- {{ t('prReview.finish') }}
315
- </UButton>
316
- <UButton
317
- color="neutral"
318
- variant="soft"
319
- :disabled="!canResolve || !hasSelection || !access.canExecuteRuns.value"
320
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
321
- data-testid="pr-review-post"
322
- @click="onResolve('post')"
323
- >
324
- {{ t('prReview.post') }}
325
- </UButton>
326
- <UButton
327
- color="primary"
328
- :loading="prReview.resolving"
329
- :disabled="!canResolve || !hasSelection || !access.canExecuteRuns.value"
330
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
331
- data-testid="pr-review-fix"
332
- @click="onResolve('fix')"
333
- >
334
- {{ t('prReview.fix') }}
335
- </UButton>
336
- </footer>
337
- </div>
255
+ <p class="mt-0.5 text-[11px] text-slate-500">
256
+ {{ f.path
257
+ }}<template v-if="f.line != null">
258
+ · {{ t('prReview.line', { line: f.line }) }}</template
259
+ >
260
+ </p>
261
+ <p class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
262
+ {{ f.detail }}
263
+ </p>
264
+ <p
265
+ v-if="f.suggestedFix"
266
+ class="mt-1 whitespace-pre-wrap rounded-md bg-slate-800/50 px-2 py-1 text-[11px] text-slate-300"
267
+ >
268
+ <span class="text-slate-500">{{ t('prReview.suggestedFix') }}</span>
269
+ {{ f.suggestedFix }}
270
+ </p>
271
+ </div>
272
+ </div>
273
+ </article>
274
+ </section>
275
+ </template>
276
+ </template>
338
277
  </div>
339
- </Teleport>
278
+
279
+ <!-- Footer -->
280
+ <footer
281
+ v-if="awaiting"
282
+ class="flex items-center justify-end gap-2 border-t border-slate-800 px-5 py-3"
283
+ >
284
+ <UButton
285
+ color="neutral"
286
+ variant="ghost"
287
+ :disabled="!canResolve || !access.canExecuteRuns.value"
288
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
289
+ data-testid="pr-review-finish"
290
+ @click="onResolve('finish')"
291
+ >
292
+ {{ t('prReview.finish') }}
293
+ </UButton>
294
+ <UButton
295
+ color="neutral"
296
+ variant="soft"
297
+ :disabled="!canResolve || !hasSelection || !access.canExecuteRuns.value"
298
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
299
+ data-testid="pr-review-post"
300
+ @click="onResolve('post')"
301
+ >
302
+ {{ t('prReview.post') }}
303
+ </UButton>
304
+ <UButton
305
+ color="primary"
306
+ :loading="prReview.resolving"
307
+ :disabled="!canResolve || !hasSelection || !access.canExecuteRuns.value"
308
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
309
+ data-testid="pr-review-fix"
310
+ @click="onResolve('fix')"
311
+ >
312
+ {{ t('prReview.fix') }}
313
+ </UButton>
314
+ </footer>
315
+ </ResultWindowShell>
340
316
  </template>
@@ -15,11 +15,9 @@ const board = useBoardStore()
15
15
  const execution = useExecutionStore()
16
16
  const { t, d } = useI18n()
17
17
 
18
- // `manageEscape: false` — `ResultWindowShell` owns Escape (and focus trap + scroll lock +
19
- // stacking) via the shared overlay behaviour.
20
- const { open, blockId, instanceId, stepIndex, close } = useResultView('ralph-loop', {
21
- manageEscape: false,
22
- })
18
+ // `ResultWindowShell` owns Escape (and focus trap + scroll lock + stacking) via the shared
19
+ // overlay behaviour.
20
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('ralph-loop')
23
21
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
24
22
  const headerTitle = computed(
25
23
  () => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,