@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.
- package/app/components/followUp/FollowUpWindow.vue +174 -200
- package/app/components/forkDecision/ForkDecisionWindow.vue +250 -275
- package/app/components/gates/GateResultView.vue +309 -333
- package/app/components/humanTest/HumanTestWindow.vue +212 -236
- package/app/components/media/ArtifactLightbox.vue +20 -19
- package/app/components/panels/GenericStructuredResultView.vue +60 -88
- package/app/components/ralph/RalphLoopResultView.vue +150 -171
- package/app/components/testing/TestReportWindow.vue +505 -559
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +224 -262
- package/i18n/locales/de.json +0 -1
- package/i18n/locales/en.json +0 -1
- package/i18n/locales/es.json +0 -1
- package/i18n/locales/fr.json +0 -1
- package/i18n/locales/he.json +0 -1
- package/i18n/locales/it.json +0 -1
- package/i18n/locales/ja.json +0 -1
- package/i18n/locales/pl.json +0 -1
- package/i18n/locales/tr.json +0 -1
- package/i18n/locales/uk.json +0 -1
- package/package.json +1 -1
- package/app/composables/useFocusTrap.ts +0 -72
|
@@ -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
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
<
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
</
|
|
338
|
+
</section>
|
|
339
|
+
</div>
|
|
373
340
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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>
|