@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.
- package/app/components/brainstorm/BrainstormWindow.vue +338 -374
- package/app/components/clarity/ClarityReviewWindow.vue +323 -353
- package/app/components/consensus/ConsensusSessionWindow.vue +138 -150
- package/app/components/docs/DocInterviewWindow.vue +108 -136
- package/app/components/followUp/FollowUpWindow.vue +3 -5
- package/app/components/forkDecision/ForkDecisionWindow.vue +1 -3
- package/app/components/gates/GateResultView.vue +3 -5
- package/app/components/humanTest/HumanTestWindow.vue +3 -5
- package/app/components/initiative/InitiativePlanningWindow.vue +89 -112
- package/app/components/initiative/InitiativeTrackerWindow.vue +389 -424
- package/app/components/panels/GenericStructuredResultView.vue +3 -5
- package/app/components/panels/MergerResultView.vue +3 -5
- package/app/components/panels/ResultWindowShell.vue +1 -1
- package/app/components/prReview/PrReviewWindow.vue +181 -205
- package/app/components/ralph/RalphLoopResultView.vue +3 -5
- package/app/components/requirements/RequirementsReviewWindow.vue +512 -557
- package/app/components/spec/ServiceSpecWindow.vue +235 -266
- package/app/components/testing/TestReportWindow.vue +4 -6
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +3 -5
- package/app/composables/useResultView.ts +10 -21
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import { computed } from 'vue'
|
|
|
10
10
|
import type { ConsensusContribution, ConsensusSession } from '~/types/consensus'
|
|
11
11
|
import CopyButton from '~/components/common/CopyButton.vue'
|
|
12
12
|
import MarkdownProse from '~/components/common/MarkdownProse.vue'
|
|
13
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
13
14
|
import { agentKindMeta } from '~/utils/catalog'
|
|
14
15
|
|
|
15
16
|
const { t, n } = useI18n()
|
|
@@ -30,6 +31,24 @@ const session = computed<ConsensusSession | null>(() =>
|
|
|
30
31
|
)
|
|
31
32
|
const loading = computed(() => (blockId.value ? consensus.isLoading(blockId.value) : false))
|
|
32
33
|
|
|
34
|
+
// Composed header: "Consensus · <strategy>" as the dialog title (with the block appended when
|
|
35
|
+
// present), and the agent kind + participant count as the subtitle line.
|
|
36
|
+
const headerTitle = computed(() => {
|
|
37
|
+
const base = session.value
|
|
38
|
+
? `${t('consensus.titlePrefix')} · ${strategyLabel(session.value.strategy)}`
|
|
39
|
+
: t('consensus.titlePrefix')
|
|
40
|
+
return block.value ? `${base} — ${block.value.title}` : base
|
|
41
|
+
})
|
|
42
|
+
const headerSubtitle = computed(() =>
|
|
43
|
+
session.value
|
|
44
|
+
? `${agentKindMeta(session.value.agentKind).label} · ${t(
|
|
45
|
+
'consensus.participantCount',
|
|
46
|
+
{ count: session.value.participants.length },
|
|
47
|
+
session.value.participants.length,
|
|
48
|
+
)}`
|
|
49
|
+
: undefined,
|
|
50
|
+
)
|
|
51
|
+
|
|
33
52
|
// Exhaustive enum→key maps (literal key strings keep the typed-key drift guard live,
|
|
34
53
|
// vs a runtime-built `consensus.strategy.${value}`).
|
|
35
54
|
const STRATEGY_LABEL_KEYS: Record<string, string> = {
|
|
@@ -94,168 +113,137 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
|
|
|
94
113
|
</script>
|
|
95
114
|
|
|
96
115
|
<template>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
<ResultWindowShell
|
|
117
|
+
:open="open"
|
|
118
|
+
icon="i-lucide-users-round"
|
|
119
|
+
icon-class="bg-amber-500/15 text-amber-300"
|
|
120
|
+
:title="headerTitle"
|
|
121
|
+
:subtitle="headerSubtitle"
|
|
122
|
+
variant="centered"
|
|
123
|
+
width="5xl"
|
|
124
|
+
@close="close"
|
|
125
|
+
>
|
|
126
|
+
<template v-if="session" #header-extras>
|
|
127
|
+
<span
|
|
128
|
+
class="rounded-full px-2.5 py-1 text-xs font-medium"
|
|
129
|
+
:class="STATUS_CLASS[session.status] ?? 'bg-slate-700 text-slate-300'"
|
|
107
130
|
>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<UIcon name="i-lucide-users-round" class="h-5 w-5 text-amber-300" />
|
|
112
|
-
</div>
|
|
113
|
-
<div class="min-w-0 flex-1">
|
|
114
|
-
<h2 class="truncate text-sm font-semibold text-slate-100">
|
|
115
|
-
{{ t('consensus.titlePrefix') }} ·
|
|
116
|
-
{{ session ? strategyLabel(session.strategy) : '' }}
|
|
117
|
-
<span v-if="block" class="font-normal text-slate-400">— {{ block.title }}</span>
|
|
118
|
-
</h2>
|
|
119
|
-
<p v-if="session" class="text-xs text-slate-500">
|
|
120
|
-
{{ agentKindMeta(session.agentKind).label }} ·
|
|
121
|
-
{{
|
|
122
|
-
t(
|
|
123
|
-
'consensus.participantCount',
|
|
124
|
-
{ count: session.participants.length },
|
|
125
|
-
session.participants.length,
|
|
126
|
-
)
|
|
127
|
-
}}
|
|
128
|
-
</p>
|
|
129
|
-
</div>
|
|
130
|
-
<span
|
|
131
|
-
v-if="session"
|
|
132
|
-
class="rounded-full px-2.5 py-1 text-xs font-medium"
|
|
133
|
-
:class="STATUS_CLASS[session.status] ?? 'bg-slate-700 text-slate-300'"
|
|
134
|
-
>
|
|
135
|
-
{{ statusLabel(session.status) }}
|
|
136
|
-
</span>
|
|
137
|
-
<button
|
|
138
|
-
class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
139
|
-
@click="close"
|
|
140
|
-
>
|
|
141
|
-
<UIcon name="i-lucide-x" class="h-5 w-5" />
|
|
142
|
-
</button>
|
|
143
|
-
</header>
|
|
131
|
+
{{ statusLabel(session.status) }}
|
|
132
|
+
</span>
|
|
133
|
+
</template>
|
|
144
134
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
135
|
+
<div class="flex-1 overflow-y-auto px-6 py-5">
|
|
136
|
+
<div v-if="loading && !session" class="py-16 text-center text-sm text-slate-500">
|
|
137
|
+
{{ t('consensus.loading') }}
|
|
138
|
+
</div>
|
|
139
|
+
<div v-else-if="!session" class="py-16 text-center text-sm text-slate-500">
|
|
140
|
+
{{ t('consensus.empty') }}
|
|
141
|
+
</div>
|
|
142
|
+
<template v-else>
|
|
143
|
+
<!-- failure -->
|
|
144
|
+
<div
|
|
145
|
+
v-if="session.status === 'failed'"
|
|
146
|
+
class="mb-5 flex items-start gap-2 rounded-lg border border-rose-800/60 bg-rose-950/40 px-4 py-3 text-sm text-rose-200"
|
|
147
|
+
>
|
|
148
|
+
<span class="min-w-0 flex-1">{{
|
|
149
|
+
t('consensus.failed', { error: session.error ?? t('consensus.unknownError') })
|
|
150
|
+
}}</span>
|
|
151
|
+
<CopyButton v-if="session.error" :text="session.error" class="-me-1 shrink-0" />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- synthesized result -->
|
|
155
|
+
<section v-if="session.synthesis" class="mb-6">
|
|
156
|
+
<div class="mb-2 flex items-center gap-2">
|
|
157
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
158
|
+
{{ t('consensus.synthesizedResult') }}
|
|
159
|
+
</h3>
|
|
160
|
+
<span
|
|
161
|
+
v-if="session.confidence != null"
|
|
162
|
+
class="rounded bg-emerald-500/15 px-1.5 py-0.5 text-xs text-emerald-300"
|
|
163
|
+
>{{ t('consensus.confidence', { pct: pct(session.confidence) }) }}</span
|
|
164
|
+
>
|
|
165
|
+
<CopyButton :text="session.synthesis" class="ms-auto -my-1" />
|
|
151
166
|
</div>
|
|
152
|
-
<
|
|
153
|
-
|
|
167
|
+
<MarkdownProse
|
|
168
|
+
:text="session.synthesis"
|
|
169
|
+
class="rounded-lg border border-slate-800 bg-slate-950/60 px-4 py-3 text-sm text-slate-200"
|
|
170
|
+
/>
|
|
171
|
+
<ul v-if="session.dissent?.length" class="mt-2 space-y-1">
|
|
172
|
+
<li
|
|
173
|
+
v-for="(d, i) in session.dissent"
|
|
174
|
+
:key="i"
|
|
175
|
+
class="flex items-start gap-2 text-xs text-amber-300/90"
|
|
176
|
+
>
|
|
177
|
+
<UIcon name="i-lucide-triangle-alert" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
178
|
+
<span>{{ d }}</span>
|
|
179
|
+
</li>
|
|
180
|
+
</ul>
|
|
181
|
+
</section>
|
|
182
|
+
|
|
183
|
+
<!-- participants -->
|
|
184
|
+
<section class="mb-6">
|
|
185
|
+
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
186
|
+
{{ t('consensus.panel') }}
|
|
187
|
+
</h3>
|
|
188
|
+
<div class="flex flex-wrap gap-2">
|
|
154
189
|
<div
|
|
155
|
-
v-
|
|
156
|
-
|
|
190
|
+
v-for="(p, i) in session.participants"
|
|
191
|
+
:key="p.id"
|
|
192
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-1.5 text-xs"
|
|
157
193
|
>
|
|
158
|
-
<span class="
|
|
159
|
-
t('consensus.
|
|
194
|
+
<span class="font-medium text-slate-200">{{
|
|
195
|
+
t('consensus.expert', { letter: String.fromCharCode(65 + i) })
|
|
160
196
|
}}</span>
|
|
161
|
-
<
|
|
197
|
+
<span class="text-slate-400"> · {{ p.role }}</span>
|
|
198
|
+
<span v-if="p.modelId" class="ms-1 text-slate-500"
|
|
199
|
+
>({{ models.labelForRef(p.modelId) ?? p.modelId }})</span
|
|
200
|
+
>
|
|
162
201
|
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</section>
|
|
163
204
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
205
|
+
<!-- rounds -->
|
|
206
|
+
<section v-for="round in session.rounds" :key="round.index" class="mb-5">
|
|
207
|
+
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
208
|
+
{{ t('consensus.round.heading', { n: round.index + 1 }) }} ·
|
|
209
|
+
{{ roundLabel(round.kind) }}
|
|
210
|
+
</h3>
|
|
211
|
+
<div class="space-y-3">
|
|
212
|
+
<div
|
|
213
|
+
v-for="c in round.contributions"
|
|
214
|
+
:key="c.participantId"
|
|
215
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 px-4 py-3"
|
|
216
|
+
>
|
|
217
|
+
<div class="mb-1 flex items-center gap-2">
|
|
218
|
+
<span class="text-xs font-semibold text-slate-200">{{
|
|
219
|
+
anonLabel(c.participantId)
|
|
220
|
+
}}</span>
|
|
221
|
+
<span class="text-xs text-slate-500">{{ roleFor(c.participantId) }}</span>
|
|
170
222
|
<span
|
|
171
|
-
v-if="
|
|
172
|
-
class="rounded bg-
|
|
173
|
-
>{{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
class="rounded-lg border border-slate-800 bg-slate-950/60 px-4 py-3 text-sm text-slate-200"
|
|
180
|
-
/>
|
|
181
|
-
<ul v-if="session.dissent?.length" class="mt-2 space-y-1">
|
|
182
|
-
<li
|
|
183
|
-
v-for="(d, i) in session.dissent"
|
|
184
|
-
:key="i"
|
|
185
|
-
class="flex items-start gap-2 text-xs text-amber-300/90"
|
|
186
|
-
>
|
|
187
|
-
<UIcon name="i-lucide-triangle-alert" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
188
|
-
<span>{{ d }}</span>
|
|
189
|
-
</li>
|
|
190
|
-
</ul>
|
|
191
|
-
</section>
|
|
192
|
-
|
|
193
|
-
<!-- participants -->
|
|
194
|
-
<section class="mb-6">
|
|
195
|
-
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
196
|
-
{{ t('consensus.panel') }}
|
|
197
|
-
</h3>
|
|
198
|
-
<div class="flex flex-wrap gap-2">
|
|
199
|
-
<div
|
|
200
|
-
v-for="(p, i) in session.participants"
|
|
201
|
-
:key="p.id"
|
|
202
|
-
class="rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-1.5 text-xs"
|
|
223
|
+
v-if="topScore(c)"
|
|
224
|
+
class="ms-auto rounded bg-slate-800 px-1.5 py-0.5 text-xs text-slate-300"
|
|
225
|
+
>{{
|
|
226
|
+
t('consensus.topScore', {
|
|
227
|
+
label: topScore(c)!.label,
|
|
228
|
+
pct: pct(topScore(c)!.value),
|
|
229
|
+
})
|
|
230
|
+
}}</span
|
|
203
231
|
>
|
|
204
|
-
|
|
205
|
-
t('consensus.expert', { letter: String.fromCharCode(65 + i) })
|
|
206
|
-
}}</span>
|
|
207
|
-
<span class="text-slate-400"> · {{ p.role }}</span>
|
|
208
|
-
<span v-if="p.modelId" class="ms-1 text-slate-500"
|
|
209
|
-
>({{ models.labelForRef(p.modelId) ?? p.modelId }})</span
|
|
210
|
-
>
|
|
211
|
-
</div>
|
|
232
|
+
<CopyButton :text="c.text" :class="topScore(c) ? '-my-1' : 'ms-auto -my-1'" />
|
|
212
233
|
</div>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
</h3>
|
|
221
|
-
<div class="space-y-3">
|
|
222
|
-
<div
|
|
223
|
-
v-for="c in round.contributions"
|
|
224
|
-
:key="c.participantId"
|
|
225
|
-
class="rounded-lg border border-slate-800 bg-slate-950/40 px-4 py-3"
|
|
234
|
+
<MarkdownProse :text="c.text" class="text-sm text-slate-300" />
|
|
235
|
+
<div v-if="c.scores?.length" class="mt-2 flex flex-wrap gap-1.5">
|
|
236
|
+
<span
|
|
237
|
+
v-for="s in c.scores"
|
|
238
|
+
:key="s.dimension"
|
|
239
|
+
class="rounded bg-slate-800/80 px-1.5 py-0.5 text-xs text-slate-400"
|
|
240
|
+
>{{ s.dimension }}: {{ pct(s.value) }}</span
|
|
226
241
|
>
|
|
227
|
-
<div class="mb-1 flex items-center gap-2">
|
|
228
|
-
<span class="text-xs font-semibold text-slate-200">{{
|
|
229
|
-
anonLabel(c.participantId)
|
|
230
|
-
}}</span>
|
|
231
|
-
<span class="text-xs text-slate-500">{{ roleFor(c.participantId) }}</span>
|
|
232
|
-
<span
|
|
233
|
-
v-if="topScore(c)"
|
|
234
|
-
class="ms-auto rounded bg-slate-800 px-1.5 py-0.5 text-xs text-slate-300"
|
|
235
|
-
>{{
|
|
236
|
-
t('consensus.topScore', {
|
|
237
|
-
label: topScore(c)!.label,
|
|
238
|
-
pct: pct(topScore(c)!.value),
|
|
239
|
-
})
|
|
240
|
-
}}</span
|
|
241
|
-
>
|
|
242
|
-
<CopyButton :text="c.text" :class="topScore(c) ? '-my-1' : 'ms-auto -my-1'" />
|
|
243
|
-
</div>
|
|
244
|
-
<MarkdownProse :text="c.text" class="text-sm text-slate-300" />
|
|
245
|
-
<div v-if="c.scores?.length" class="mt-2 flex flex-wrap gap-1.5">
|
|
246
|
-
<span
|
|
247
|
-
v-for="s in c.scores"
|
|
248
|
-
:key="s.dimension"
|
|
249
|
-
class="rounded bg-slate-800/80 px-1.5 py-0.5 text-xs text-slate-400"
|
|
250
|
-
>{{ s.dimension }}: {{ pct(s.value) }}</span
|
|
251
|
-
>
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
242
|
</div>
|
|
255
|
-
</
|
|
256
|
-
</
|
|
257
|
-
</
|
|
258
|
-
</
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</section>
|
|
246
|
+
</template>
|
|
259
247
|
</div>
|
|
260
|
-
</
|
|
248
|
+
</ResultWindowShell>
|
|
261
249
|
</template>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// `doc-interviewer` step's result view. Live `docInterview` stream events patch the store, so an
|
|
9
9
|
// open window follows the interview as it progresses. Mirrors InitiativePlanningWindow.vue.
|
|
10
10
|
import { computed, reactive, watch } from 'vue'
|
|
11
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
11
12
|
|
|
12
13
|
const board = useBoardStore()
|
|
13
14
|
const docInterview = useDocInterviewStore()
|
|
@@ -67,147 +68,118 @@ const onProceed = () => flushThen((id) => docInterview.proceedInterview(id))
|
|
|
67
68
|
</script>
|
|
68
69
|
|
|
69
70
|
<template>
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
<ResultWindowShell
|
|
72
|
+
:open="open"
|
|
73
|
+
icon="i-lucide-messages-square"
|
|
74
|
+
icon-class="bg-indigo-500/15 text-indigo-300"
|
|
75
|
+
:title="block?.title ?? t('docInterview.title')"
|
|
76
|
+
:subtitle="t('docInterview.subtitle')"
|
|
77
|
+
width="3xl"
|
|
78
|
+
testid="doc-interview-window"
|
|
79
|
+
@close="close"
|
|
80
|
+
>
|
|
81
|
+
<template v-if="session" #header-extras>
|
|
82
|
+
<UBadge :color="converged ? 'success' : 'primary'" variant="subtle" size="sm">
|
|
83
|
+
{{ t(converged ? 'docInterview.status.done' : 'docInterview.status.awaiting') }}
|
|
84
|
+
</UBadge>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
88
|
+
<!-- No session yet -->
|
|
76
89
|
<div
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
aria-modal="true"
|
|
80
|
-
data-testid="doc-interview-window"
|
|
90
|
+
v-if="!session"
|
|
91
|
+
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
81
92
|
>
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{{
|
|
104
|
-
</UBadge>
|
|
105
|
-
<button
|
|
106
|
-
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
107
|
-
@click="close"
|
|
108
|
-
>
|
|
109
|
-
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
110
|
-
</button>
|
|
111
|
-
</header>
|
|
112
|
-
|
|
113
|
-
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
114
|
-
<!-- No session yet -->
|
|
115
|
-
<div
|
|
116
|
-
v-if="!session"
|
|
117
|
-
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
93
|
+
<UIcon name="i-lucide-messages-square" class="h-8 w-8 opacity-40" />
|
|
94
|
+
<p class="text-sm">{{ t('docInterview.empty') }}</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<template v-else>
|
|
98
|
+
<p class="mb-4 text-[13px] leading-relaxed text-slate-300">
|
|
99
|
+
{{ t('docInterview.intro') }}
|
|
100
|
+
</p>
|
|
101
|
+
|
|
102
|
+
<!-- Converged: show the synthesized authoring brief -->
|
|
103
|
+
<div
|
|
104
|
+
v-if="converged"
|
|
105
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 p-4"
|
|
106
|
+
data-testid="doc-interview-converged"
|
|
107
|
+
>
|
|
108
|
+
<p class="mb-2 text-[11px] font-medium uppercase tracking-wide text-slate-500">
|
|
109
|
+
{{ t('docInterview.brief') }}
|
|
110
|
+
</p>
|
|
111
|
+
<pre
|
|
112
|
+
v-if="session.brief"
|
|
113
|
+
class="whitespace-pre-wrap break-words text-[13px] leading-relaxed text-slate-300"
|
|
114
|
+
>{{ session.brief }}</pre
|
|
118
115
|
>
|
|
119
|
-
|
|
120
|
-
<p class="text-sm">{{ t('docInterview.empty') }}</p>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<template v-else>
|
|
124
|
-
<p class="mb-4 text-[13px] leading-relaxed text-slate-300">
|
|
125
|
-
{{ t('docInterview.intro') }}
|
|
126
|
-
</p>
|
|
127
|
-
|
|
128
|
-
<!-- Converged: show the synthesized authoring brief -->
|
|
129
|
-
<div
|
|
130
|
-
v-if="converged"
|
|
131
|
-
class="rounded-lg border border-slate-800 bg-slate-950/40 p-4"
|
|
132
|
-
data-testid="doc-interview-converged"
|
|
133
|
-
>
|
|
134
|
-
<p class="mb-2 text-[11px] font-medium uppercase tracking-wide text-slate-500">
|
|
135
|
-
{{ t('docInterview.brief') }}
|
|
136
|
-
</p>
|
|
137
|
-
<pre
|
|
138
|
-
v-if="session.brief"
|
|
139
|
-
class="whitespace-pre-wrap break-words text-[13px] leading-relaxed text-slate-300"
|
|
140
|
-
>{{ session.brief }}</pre
|
|
141
|
-
>
|
|
142
|
-
<p v-else class="text-[13px] text-slate-400">{{ t('docInterview.converged') }}</p>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<!-- No pending questions but not yet converged -->
|
|
146
|
-
<div
|
|
147
|
-
v-else-if="questions.length === 0"
|
|
148
|
-
class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
|
|
149
|
-
>
|
|
150
|
-
{{ t('docInterview.converged') }}
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<!-- Interview questions -->
|
|
154
|
-
<ul v-else class="space-y-4">
|
|
155
|
-
<li
|
|
156
|
-
v-for="q in questions"
|
|
157
|
-
:key="q.key"
|
|
158
|
-
class="rounded-lg border border-slate-800 bg-slate-950/40 p-3"
|
|
159
|
-
data-testid="doc-interview-question"
|
|
160
|
-
>
|
|
161
|
-
<p class="mb-2 text-[13px] font-medium text-slate-200">{{ q.question }}</p>
|
|
162
|
-
<UTextarea
|
|
163
|
-
v-model="drafts[q.key]"
|
|
164
|
-
:rows="2"
|
|
165
|
-
autoresize
|
|
166
|
-
:placeholder="t('docInterview.answerPlaceholder')"
|
|
167
|
-
class="w-full"
|
|
168
|
-
data-testid="doc-interview-answer"
|
|
169
|
-
@blur="persist(q)"
|
|
170
|
-
/>
|
|
171
|
-
</li>
|
|
172
|
-
</ul>
|
|
173
|
-
</template>
|
|
116
|
+
<p v-else class="text-[13px] text-slate-400">{{ t('docInterview.converged') }}</p>
|
|
174
117
|
</div>
|
|
175
118
|
|
|
176
|
-
<!--
|
|
177
|
-
<
|
|
178
|
-
v-if="
|
|
179
|
-
class="
|
|
119
|
+
<!-- No pending questions but not yet converged -->
|
|
120
|
+
<div
|
|
121
|
+
v-else-if="questions.length === 0"
|
|
122
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 p-4 text-center text-[13px] text-slate-400"
|
|
180
123
|
>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
>
|
|
206
|
-
{{ t('docInterview.continue') }}
|
|
207
|
-
</UButton>
|
|
208
|
-
</div>
|
|
209
|
-
</footer>
|
|
210
|
-
</div>
|
|
124
|
+
{{ t('docInterview.converged') }}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- Interview questions -->
|
|
128
|
+
<ul v-else class="space-y-4">
|
|
129
|
+
<li
|
|
130
|
+
v-for="q in questions"
|
|
131
|
+
:key="q.key"
|
|
132
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 p-3"
|
|
133
|
+
data-testid="doc-interview-question"
|
|
134
|
+
>
|
|
135
|
+
<p class="mb-2 text-[13px] font-medium text-slate-200">{{ q.question }}</p>
|
|
136
|
+
<UTextarea
|
|
137
|
+
v-model="drafts[q.key]"
|
|
138
|
+
:rows="2"
|
|
139
|
+
autoresize
|
|
140
|
+
:placeholder="t('docInterview.answerPlaceholder')"
|
|
141
|
+
class="w-full"
|
|
142
|
+
data-testid="doc-interview-answer"
|
|
143
|
+
@blur="persist(q)"
|
|
144
|
+
/>
|
|
145
|
+
</li>
|
|
146
|
+
</ul>
|
|
147
|
+
</template>
|
|
211
148
|
</div>
|
|
212
|
-
|
|
149
|
+
|
|
150
|
+
<!-- Action rail -->
|
|
151
|
+
<footer
|
|
152
|
+
v-if="session && !converged && questions.length > 0"
|
|
153
|
+
class="flex items-center justify-between gap-3 border-t border-slate-800 px-5 py-3"
|
|
154
|
+
>
|
|
155
|
+
<p class="text-[11px] text-slate-500">
|
|
156
|
+
{{ t('docInterview.hint') }}
|
|
157
|
+
</p>
|
|
158
|
+
<div class="flex items-center gap-2">
|
|
159
|
+
<UButton
|
|
160
|
+
color="neutral"
|
|
161
|
+
variant="ghost"
|
|
162
|
+
size="sm"
|
|
163
|
+
:loading="resuming"
|
|
164
|
+
:disabled="!access.canExecuteRuns.value"
|
|
165
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
166
|
+
data-testid="doc-interview-proceed"
|
|
167
|
+
@click="onProceed"
|
|
168
|
+
>
|
|
169
|
+
{{ t('docInterview.proceed') }}
|
|
170
|
+
</UButton>
|
|
171
|
+
<UButton
|
|
172
|
+
color="primary"
|
|
173
|
+
size="sm"
|
|
174
|
+
:loading="resuming"
|
|
175
|
+
:disabled="!allAnswered || !access.canExecuteRuns.value"
|
|
176
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
177
|
+
data-testid="doc-interview-continue"
|
|
178
|
+
@click="onContinue"
|
|
179
|
+
>
|
|
180
|
+
{{ t('docInterview.continue') }}
|
|
181
|
+
</UButton>
|
|
182
|
+
</div>
|
|
183
|
+
</footer>
|
|
184
|
+
</ResultWindowShell>
|
|
213
185
|
</template>
|
|
@@ -21,11 +21,9 @@ const access = useWorkspaceAccess()
|
|
|
21
21
|
|
|
22
22
|
const { t } = useI18n()
|
|
23
23
|
|
|
24
|
-
// `
|
|
25
|
-
//
|
|
26
|
-
const { open, blockId, instanceId, stepIndex, close } = useResultView('follow-ups'
|
|
27
|
-
manageEscape: false,
|
|
28
|
-
})
|
|
24
|
+
// `ResultWindowShell` owns Escape (and focus trap + scroll lock + stacking) via the shared
|
|
25
|
+
// overlay behaviour.
|
|
26
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('follow-ups')
|
|
29
27
|
|
|
30
28
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
31
29
|
const headerTitle = computed(() =>
|
|
@@ -24,11 +24,9 @@ const access = useWorkspaceAccess()
|
|
|
24
24
|
const { t } = useI18n()
|
|
25
25
|
|
|
26
26
|
// Hybrid: state rides the coder step (like follow-ups), but warm it from the GET on open too.
|
|
27
|
-
//
|
|
28
|
-
// stacking). No `stepRef`: this is a pre-run decision, so there's no "restart from here".
|
|
27
|
+
// No `stepRef`: this is a pre-run decision, so there's no "restart from here".
|
|
29
28
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('fork-decision', {
|
|
30
29
|
onOpen: (id) => void forkDecision.load(id),
|
|
31
|
-
manageEscape: false,
|
|
32
30
|
})
|
|
33
31
|
|
|
34
32
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
@@ -20,11 +20,9 @@ const { t } = useI18n()
|
|
|
20
20
|
const access = useWorkspaceAccess()
|
|
21
21
|
|
|
22
22
|
// Synchronous window: it reads its state straight off the execution step, so there's
|
|
23
|
-
// nothing to fetch on open (no `onOpen` loader). `
|
|
24
|
-
//
|
|
25
|
-
const { open, blockId, instanceId, stepIndex, close } = useResultView('gate'
|
|
26
|
-
manageEscape: false,
|
|
27
|
-
})
|
|
23
|
+
// nothing to fetch on open (no `onOpen` loader). `ResultWindowShell` owns Escape (and focus
|
|
24
|
+
// trap + scroll lock + stacking).
|
|
25
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('gate')
|
|
28
26
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
29
27
|
const prUrl = computed(() => block.value?.pullRequest?.url ?? null)
|
|
30
28
|
const headerTitle = computed(
|
|
@@ -25,11 +25,9 @@ const access = useWorkspaceAccess()
|
|
|
25
25
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
26
26
|
|
|
27
27
|
// Shared seam contract (open/blockId/close). No `onOpen` loader: the gate state rides on the
|
|
28
|
-
// execution step, pushed over the stream. `
|
|
29
|
-
//
|
|
30
|
-
const { open, blockId, instanceId, stepIndex, close } = useResultView('human-test'
|
|
31
|
-
manageEscape: false,
|
|
32
|
-
})
|
|
28
|
+
// execution step, pushed over the stream. `ResultWindowShell` owns Escape (and focus trap +
|
|
29
|
+
// scroll lock + stacking).
|
|
30
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('human-test')
|
|
33
31
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
34
32
|
const headerTitle = computed(() =>
|
|
35
33
|
block.value ? t('humanTest.titleWithBlock', { title: block.value.title }) : t('humanTest.title'),
|