@cat-factory/app 0.137.2 → 0.138.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.
@@ -14,6 +14,7 @@ import { useBoardStore } from '~/stores/board'
14
14
  import { useForkDecisionStore } from '~/stores/forkDecision'
15
15
  import type { ForkChatMessage, ForkDecisionStepState, ForkOption } from '~/types/execution'
16
16
  import { FORK_DECISION_META } from '~/utils/catalog'
17
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
17
18
 
18
19
  const execution = useExecutionStore()
19
20
  const board = useBoardStore()
@@ -23,11 +24,19 @@ const access = useWorkspaceAccess()
23
24
  const { t } = useI18n()
24
25
 
25
26
  // Hybrid: state rides the coder step (like follow-ups), but warm it from the GET on open too.
27
+ // `manageEscape: false` — `ResultWindowShell` owns Escape (and focus trap + scroll lock +
28
+ // stacking). No `stepRef`: this is a pre-run decision, so there's no "restart from here".
26
29
  const { open, blockId, instanceId, stepIndex, close } = useResultView('fork-decision', {
27
30
  onOpen: (id) => void forkDecision.load(id),
31
+ manageEscape: false,
28
32
  })
29
33
 
30
34
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
35
+ const headerTitle = computed(() =>
36
+ block.value
37
+ ? t('forkDecision.titleWithBlock', { title: block.value.title })
38
+ : t('forkDecision.title'),
39
+ )
31
40
  const instance = computed(() =>
32
41
  instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
33
42
  )
@@ -100,298 +109,264 @@ async function onSend() {
100
109
  </script>
101
110
 
102
111
  <template>
103
- <Teleport to="body">
104
- <div
105
- v-if="open"
106
- data-testid="fork-decision-window"
107
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
108
- @click.self="close"
109
- >
112
+ <ResultWindowShell
113
+ :open="open"
114
+ :icon="FORK_DECISION_META.icon"
115
+ icon-class="bg-violet-500/15 text-violet-300"
116
+ :title="headerTitle"
117
+ :subtitle="t('forkDecision.subtitle')"
118
+ width="3xl"
119
+ testid="fork-decision-window"
120
+ @close="close"
121
+ >
122
+ <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
123
+ <!-- Proposing: the read-only proposer is still working. -->
110
124
  <div
111
- class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
112
- role="dialog"
113
- aria-modal="true"
125
+ v-if="status === 'proposing'"
126
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
114
127
  >
115
- <!-- Header -->
116
- <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
117
- <span
118
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500/15 text-violet-300"
119
- >
120
- <UIcon :name="FORK_DECISION_META.icon" class="h-4 w-4" />
121
- </span>
122
- <div class="min-w-0 flex-1">
123
- <h2 class="truncate text-sm font-semibold text-slate-100">
124
- {{
125
- block
126
- ? t('forkDecision.titleWithBlock', { title: block.title })
127
- : t('forkDecision.title')
128
- }}
129
- </h2>
130
- <p class="truncate text-[11px] text-slate-400">{{ t('forkDecision.subtitle') }}</p>
131
- </div>
132
- <button
133
- class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
134
- @click="close"
135
- >
136
- <UIcon name="i-lucide-x" class="h-4 w-4" />
137
- </button>
138
- </header>
139
-
140
- <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
141
- <!-- Proposing: the read-only proposer is still working. -->
142
- <div
143
- v-if="status === 'proposing'"
144
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
145
- >
146
- <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
147
- <p class="text-sm">{{ t('forkDecision.proposing.title') }}</p>
148
- <p class="max-w-sm text-[11px] text-slate-500">
149
- {{ t('forkDecision.proposing.hint') }}
150
- </p>
151
- </div>
128
+ <UIcon name="i-lucide-loader-circle" class="h-8 w-8 animate-spin opacity-60" />
129
+ <p class="text-sm">{{ t('forkDecision.proposing.title') }}</p>
130
+ <p class="max-w-sm text-[11px] text-slate-500">
131
+ {{ t('forkDecision.proposing.hint') }}
132
+ </p>
133
+ </div>
152
134
 
153
- <!-- A single path (no materially different alternatives): a read-only record. -->
154
- <div
155
- v-else-if="status === 'single_path'"
156
- class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-3 text-slate-300"
157
- >
158
- <p class="text-[13px] font-medium text-slate-100">
159
- {{ t('forkDecision.singlePath.title') }}
160
- </p>
161
- <p v-if="state?.singlePathReason" class="mt-1 text-[12px]">
162
- {{ state.singlePathReason }}
163
- </p>
164
- </div>
135
+ <!-- A single path (no materially different alternatives): a read-only record. -->
136
+ <div
137
+ v-else-if="status === 'single_path'"
138
+ class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-3 text-slate-300"
139
+ >
140
+ <p class="text-[13px] font-medium text-slate-100">
141
+ {{ t('forkDecision.singlePath.title') }}
142
+ </p>
143
+ <p v-if="state?.singlePathReason" class="mt-1 text-[12px]">
144
+ {{ state.singlePathReason }}
145
+ </p>
146
+ </div>
165
147
 
166
- <!-- Chosen: a read-only record of what was decided. -->
167
- <div
168
- v-else-if="status === 'chosen'"
169
- class="rounded-xl border border-violet-500/40 bg-slate-900/60 px-4 py-3 text-slate-300"
170
- >
171
- <p class="text-[13px] font-medium text-violet-200">
172
- {{ t('forkDecision.chosen.title') }}
173
- </p>
174
- <p v-if="state?.chosen?.custom" class="mt-1 whitespace-pre-wrap text-[12px]">
175
- {{ state.chosen.custom }}
176
- </p>
177
- <p v-else-if="state?.chosen?.forkId" class="mt-1 text-[12px]">
178
- {{ forks.find((f) => f.id === state?.chosen?.forkId)?.title }}
179
- </p>
180
- <p v-if="state?.chosen?.note" class="mt-1 text-[11px] text-slate-400">
181
- {{ t('forkDecision.chosen.note', { note: state.chosen.note }) }}
182
- </p>
183
- </div>
148
+ <!-- Chosen: a read-only record of what was decided. -->
149
+ <div
150
+ v-else-if="status === 'chosen'"
151
+ class="rounded-xl border border-violet-500/40 bg-slate-900/60 px-4 py-3 text-slate-300"
152
+ >
153
+ <p class="text-[13px] font-medium text-violet-200">
154
+ {{ t('forkDecision.chosen.title') }}
155
+ </p>
156
+ <p v-if="state?.chosen?.custom" class="mt-1 whitespace-pre-wrap text-[12px]">
157
+ {{ state.chosen.custom }}
158
+ </p>
159
+ <p v-else-if="state?.chosen?.forkId" class="mt-1 text-[12px]">
160
+ {{ forks.find((f) => f.id === state?.chosen?.forkId)?.title }}
161
+ </p>
162
+ <p v-if="state?.chosen?.note" class="mt-1 text-[11px] text-slate-400">
163
+ {{ t('forkDecision.chosen.note', { note: state.chosen.note }) }}
164
+ </p>
165
+ </div>
184
166
 
185
- <!-- Awaiting the human's choice (or answering a chat turn). -->
186
- <div v-else-if="interactive" class="space-y-3">
187
- <p
188
- v-if="forkDecision.error"
189
- class="rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
190
- >
191
- {{ forkDecision.error }}
192
- </p>
167
+ <!-- Awaiting the human's choice (or answering a chat turn). -->
168
+ <div v-else-if="interactive" class="space-y-3">
169
+ <p
170
+ v-if="forkDecision.error"
171
+ class="rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
172
+ >
173
+ {{ forkDecision.error }}
174
+ </p>
193
175
 
194
- <p
195
- v-if="state?.seamSummary"
196
- class="rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
197
- >
198
- <span class="text-slate-500">{{ t('forkDecision.seam') }}</span>
199
- {{ state.seamSummary }}
200
- </p>
176
+ <p
177
+ v-if="state?.seamSummary"
178
+ class="rounded-md bg-slate-800/50 px-3 py-2 text-[12px] text-slate-300"
179
+ >
180
+ <span class="text-slate-500">{{ t('forkDecision.seam') }}</span>
181
+ {{ state.seamSummary }}
182
+ </p>
201
183
 
202
- <!-- Proposed fork cards -->
203
- <article
204
- v-for="fork in forks"
205
- :key="fork.id"
206
- data-testid="fork-option-card"
207
- class="cursor-pointer rounded-xl border px-4 py-3 transition"
208
- :class="
209
- selected === fork.id
210
- ? 'border-violet-500/70 bg-violet-500/5'
211
- : 'border-slate-800 bg-slate-900/60 hover:border-slate-700'
212
- "
213
- @click="selected = fork.id"
214
- >
215
- <div class="flex items-start gap-2">
216
- <input
217
- type="radio"
218
- class="mt-1 accent-violet-500"
219
- :checked="selected === fork.id"
220
- @change="selected = fork.id"
221
- />
222
- <div class="min-w-0 flex-1">
223
- <div class="flex items-center gap-2">
224
- <h3 class="min-w-0 flex-1 text-[13px] font-medium text-slate-100">
225
- {{ fork.title }}
226
- </h3>
227
- <UBadge v-if="fork.recommended" color="primary" variant="subtle" size="sm">
228
- {{ t('forkDecision.recommended') }}
229
- </UBadge>
230
- </div>
231
- <p v-if="fork.summary" class="mt-0.5 text-[12px] text-slate-400">
232
- {{ fork.summary }}
233
- </p>
234
- <p class="mt-1.5 whitespace-pre-wrap text-[12px] text-slate-300">
235
- {{ fork.approach }}
236
- </p>
237
- <ul v-if="fork.tradeoffs.length" class="mt-1.5 space-y-0.5">
238
- <li
239
- v-for="(tr, i) in fork.tradeoffs"
240
- :key="i"
241
- class="flex gap-1.5 text-[11px] text-slate-400"
242
- >
243
- <span class="text-slate-600">•</span>{{ tr }}
244
- </li>
245
- </ul>
246
- <p v-if="fork.riskNotes" class="mt-1.5 text-[11px] text-amber-300/90">
247
- <span class="text-amber-500/70">{{ t('forkDecision.riskNotes') }}</span>
248
- {{ fork.riskNotes }}
249
- </p>
250
- </div>
184
+ <!-- Proposed fork cards -->
185
+ <article
186
+ v-for="fork in forks"
187
+ :key="fork.id"
188
+ data-testid="fork-option-card"
189
+ class="cursor-pointer rounded-xl border px-4 py-3 transition"
190
+ :class="
191
+ selected === fork.id
192
+ ? 'border-violet-500/70 bg-violet-500/5'
193
+ : 'border-slate-800 bg-slate-900/60 hover:border-slate-700'
194
+ "
195
+ @click="selected = fork.id"
196
+ >
197
+ <div class="flex items-start gap-2">
198
+ <input
199
+ type="radio"
200
+ class="mt-1 accent-violet-500"
201
+ :checked="selected === fork.id"
202
+ @change="selected = fork.id"
203
+ />
204
+ <div class="min-w-0 flex-1">
205
+ <div class="flex items-center gap-2">
206
+ <h3 class="min-w-0 flex-1 text-[13px] font-medium text-slate-100">
207
+ {{ fork.title }}
208
+ </h3>
209
+ <UBadge v-if="fork.recommended" color="primary" variant="subtle" size="sm">
210
+ {{ t('forkDecision.recommended') }}
211
+ </UBadge>
251
212
  </div>
252
- </article>
213
+ <p v-if="fork.summary" class="mt-0.5 text-[12px] text-slate-400">
214
+ {{ fork.summary }}
215
+ </p>
216
+ <p class="mt-1.5 whitespace-pre-wrap text-[12px] text-slate-300">
217
+ {{ fork.approach }}
218
+ </p>
219
+ <ul v-if="fork.tradeoffs.length" class="mt-1.5 space-y-0.5">
220
+ <li
221
+ v-for="(tr, i) in fork.tradeoffs"
222
+ :key="i"
223
+ class="flex gap-1.5 text-[11px] text-slate-400"
224
+ >
225
+ <span class="text-slate-600">•</span>{{ tr }}
226
+ </li>
227
+ </ul>
228
+ <p v-if="fork.riskNotes" class="mt-1.5 text-[11px] text-amber-300/90">
229
+ <span class="text-amber-500/70">{{ t('forkDecision.riskNotes') }}</span>
230
+ {{ fork.riskNotes }}
231
+ </p>
232
+ </div>
233
+ </div>
234
+ </article>
253
235
 
254
- <!-- Custom approach -->
255
- <article
256
- class="rounded-xl border px-4 py-3 transition"
257
- :class="
258
- selected === 'custom'
259
- ? 'border-violet-500/70 bg-violet-500/5'
260
- : 'border-slate-800 bg-slate-900/60'
261
- "
262
- >
263
- <label class="flex cursor-pointer items-center gap-2" @click="selected = 'custom'">
264
- <input type="radio" class="accent-violet-500" :checked="selected === 'custom'" />
265
- <span class="text-[13px] font-medium text-slate-100">{{
266
- t('forkDecision.custom.title')
267
- }}</span>
268
- </label>
269
- <textarea
270
- v-model="customText"
271
- data-testid="fork-custom-input"
272
- rows="3"
273
- :placeholder="t('forkDecision.custom.placeholder')"
274
- class="mt-2 w-full resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-violet-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60"
275
- @focus="selected = 'custom'"
276
- />
277
- </article>
236
+ <!-- Custom approach -->
237
+ <article
238
+ class="rounded-xl border px-4 py-3 transition"
239
+ :class="
240
+ selected === 'custom'
241
+ ? 'border-violet-500/70 bg-violet-500/5'
242
+ : 'border-slate-800 bg-slate-900/60'
243
+ "
244
+ >
245
+ <label class="flex cursor-pointer items-center gap-2" @click="selected = 'custom'">
246
+ <input type="radio" class="accent-violet-500" :checked="selected === 'custom'" />
247
+ <span class="text-[13px] font-medium text-slate-100">{{
248
+ t('forkDecision.custom.title')
249
+ }}</span>
250
+ </label>
251
+ <textarea
252
+ v-model="customText"
253
+ data-testid="fork-custom-input"
254
+ rows="3"
255
+ :placeholder="t('forkDecision.custom.placeholder')"
256
+ class="mt-2 w-full resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-violet-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60"
257
+ @focus="selected = 'custom'"
258
+ />
259
+ </article>
278
260
 
279
- <!-- Optional steering note -->
280
- <div>
281
- <label class="mb-1 block text-[11px] text-slate-400">{{
282
- t('forkDecision.noteLabel')
283
- }}</label>
284
- <input
285
- v-model="note"
286
- type="text"
287
- :placeholder="t('forkDecision.notePlaceholder')"
288
- class="w-full rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-violet-500 focus:outline-none"
289
- />
290
- </div>
261
+ <!-- Optional steering note -->
262
+ <div>
263
+ <label class="mb-1 block text-[11px] text-slate-400">{{
264
+ t('forkDecision.noteLabel')
265
+ }}</label>
266
+ <input
267
+ v-model="note"
268
+ type="text"
269
+ :placeholder="t('forkDecision.notePlaceholder')"
270
+ class="w-full rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-violet-500 focus:outline-none"
271
+ />
272
+ </div>
291
273
 
292
- <!-- Grounded chat: ask about the forks before deciding. -->
293
- <section class="rounded-xl border border-slate-800 bg-slate-900/40 px-4 py-3">
294
- <p class="text-[11px] font-medium text-slate-400">
295
- {{ t('forkDecision.chat.title') }}
274
+ <!-- Grounded chat: ask about the forks before deciding. -->
275
+ <section class="rounded-xl border border-slate-800 bg-slate-900/40 px-4 py-3">
276
+ <p class="text-[11px] font-medium text-slate-400">
277
+ {{ t('forkDecision.chat.title') }}
278
+ </p>
279
+ <div v-if="chat.length || answering" class="mt-2 max-h-64 space-y-2 overflow-y-auto pr-1">
280
+ <div
281
+ v-for="msg in chat"
282
+ :key="msg.id"
283
+ data-testid="fork-chat-message"
284
+ class="flex"
285
+ :class="msg.role === 'human' ? 'justify-end' : 'justify-start'"
286
+ >
287
+ <p
288
+ class="max-w-[85%] whitespace-pre-wrap rounded-lg px-3 py-1.5 text-[12px]"
289
+ :class="
290
+ msg.role === 'human'
291
+ ? 'bg-violet-500/15 text-violet-100'
292
+ : 'bg-slate-800/70 text-slate-200'
293
+ "
294
+ >
295
+ {{ msg.text }}
296
296
  </p>
297
- <div
298
- v-if="chat.length || answering"
299
- class="mt-2 max-h-64 space-y-2 overflow-y-auto pr-1"
297
+ </div>
298
+ <div v-if="answering" class="flex justify-start">
299
+ <p
300
+ class="flex items-center gap-1.5 rounded-lg bg-slate-800/70 px-3 py-1.5 text-[12px] text-slate-400"
300
301
  >
301
- <div
302
- v-for="msg in chat"
303
- :key="msg.id"
304
- data-testid="fork-chat-message"
305
- class="flex"
306
- :class="msg.role === 'human' ? 'justify-end' : 'justify-start'"
307
- >
308
- <p
309
- class="max-w-[85%] whitespace-pre-wrap rounded-lg px-3 py-1.5 text-[12px]"
310
- :class="
311
- msg.role === 'human'
312
- ? 'bg-violet-500/15 text-violet-100'
313
- : 'bg-slate-800/70 text-slate-200'
314
- "
315
- >
316
- {{ msg.text }}
317
- </p>
318
- </div>
319
- <div v-if="answering" class="flex justify-start">
320
- <p
321
- class="flex items-center gap-1.5 rounded-lg bg-slate-800/70 px-3 py-1.5 text-[12px] text-slate-400"
322
- >
323
- <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
324
- {{ t('forkDecision.chat.thinking') }}
325
- </p>
326
- </div>
327
- </div>
328
- <p v-else class="mt-1 text-[11px] text-slate-500">
329
- {{ t('forkDecision.chat.hint') }}
302
+ <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
303
+ {{ t('forkDecision.chat.thinking') }}
330
304
  </p>
331
- <div class="mt-2 flex items-end gap-2">
332
- <textarea
333
- v-model="chatInput"
334
- data-testid="fork-chat-input"
335
- rows="2"
336
- :disabled="!canChat"
337
- :placeholder="
338
- chatBudgetSpent
339
- ? t('forkDecision.chat.budgetSpent')
340
- : t('forkDecision.chat.placeholder')
341
- "
342
- class="min-h-0 flex-1 resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-violet-500 focus:outline-none disabled:opacity-50"
343
- @keydown.enter.exact.prevent="onSend"
344
- />
345
- <UButton
346
- data-testid="fork-chat-send"
347
- color="neutral"
348
- variant="soft"
349
- size="sm"
350
- icon="i-lucide-send"
351
- :loading="forkDecision.chatting"
352
- :disabled="
353
- !canChat || chatInput.trim().length === 0 || !access.canExecuteRuns.value
354
- "
355
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
356
- @click="onSend"
357
- >
358
- {{ t('forkDecision.chat.send') }}
359
- </UButton>
360
- </div>
361
- </section>
305
+ </div>
362
306
  </div>
363
-
364
- <!-- Skipped / no state: nothing to decide. -->
365
- <div
366
- v-else
367
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
368
- >
369
- <UIcon :name="FORK_DECISION_META.icon" class="h-8 w-8 opacity-40" />
370
- <p class="text-sm">{{ t('forkDecision.empty.title') }}</p>
307
+ <p v-else class="mt-1 text-[11px] text-slate-500">
308
+ {{ t('forkDecision.chat.hint') }}
309
+ </p>
310
+ <div class="mt-2 flex items-end gap-2">
311
+ <textarea
312
+ v-model="chatInput"
313
+ data-testid="fork-chat-input"
314
+ rows="2"
315
+ :disabled="!canChat"
316
+ :placeholder="
317
+ chatBudgetSpent
318
+ ? t('forkDecision.chat.budgetSpent')
319
+ : t('forkDecision.chat.placeholder')
320
+ "
321
+ class="min-h-0 flex-1 resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-violet-500 focus:outline-none disabled:opacity-50"
322
+ @keydown.enter.exact.prevent="onSend"
323
+ />
324
+ <UButton
325
+ data-testid="fork-chat-send"
326
+ color="neutral"
327
+ variant="soft"
328
+ size="sm"
329
+ icon="i-lucide-send"
330
+ :loading="forkDecision.chatting"
331
+ :disabled="!canChat || chatInput.trim().length === 0 || !access.canExecuteRuns.value"
332
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
333
+ @click="onSend"
334
+ >
335
+ {{ t('forkDecision.chat.send') }}
336
+ </UButton>
371
337
  </div>
372
- </div>
338
+ </section>
339
+ </div>
373
340
 
374
- <footer
375
- v-if="interactive"
376
- class="flex items-center justify-end gap-2 border-t border-slate-800 px-5 py-3"
377
- >
378
- <UButton color="neutral" variant="ghost" size="sm" @click="close">
379
- {{ t('common.cancel') }}
380
- </UButton>
381
- <UButton
382
- data-testid="fork-option-choose"
383
- color="primary"
384
- size="sm"
385
- icon="i-lucide-check"
386
- :loading="forkDecision.choosing"
387
- :disabled="!canChoose || !access.canExecuteRuns.value"
388
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
389
- @click="onChoose"
390
- >
391
- {{ t('forkDecision.choose') }}
392
- </UButton>
393
- </footer>
341
+ <!-- Skipped / no state: nothing to decide. -->
342
+ <div
343
+ v-else
344
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
345
+ >
346
+ <UIcon :name="FORK_DECISION_META.icon" class="h-8 w-8 opacity-40" />
347
+ <p class="text-sm">{{ t('forkDecision.empty.title') }}</p>
394
348
  </div>
395
349
  </div>
396
- </Teleport>
350
+
351
+ <footer
352
+ v-if="interactive"
353
+ class="flex items-center justify-end gap-2 border-t border-slate-800 px-5 py-3"
354
+ >
355
+ <UButton color="neutral" variant="ghost" size="sm" @click="close">
356
+ {{ t('common.cancel') }}
357
+ </UButton>
358
+ <UButton
359
+ data-testid="fork-option-choose"
360
+ color="primary"
361
+ size="sm"
362
+ icon="i-lucide-check"
363
+ :loading="forkDecision.choosing"
364
+ :disabled="!canChoose || !access.canExecuteRuns.value"
365
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
366
+ @click="onChoose"
367
+ >
368
+ {{ t('forkDecision.choose') }}
369
+ </UButton>
370
+ </footer>
371
+ </ResultWindowShell>
397
372
  </template>