@cat-factory/app 0.138.1 → 0.140.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/board/AddTaskModal.vue +12 -39
- 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/documents/ContextDocumentPicker.vue +89 -70
- package/app/components/documents/RepoContextDocPicker.vue +277 -0
- 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/github/RepoTreeBrowser.vue +19 -10
- 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/panels/inspector/TaskRunSettings.vue +16 -23
- package/app/components/pipeline/PipelineBuilder.vue +10 -0
- package/app/components/pipeline/PipelinePicker.vue +120 -0
- package/app/components/pipeline/PipelinePreview.vue +49 -0
- 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/api/github.ts +8 -0
- package/app/composables/useContextLinking.spec.ts +138 -0
- package/app/composables/useContextLinking.ts +120 -8
- package/app/composables/useCopyToClipboard.spec.ts +27 -0
- package/app/composables/useCopyToClipboard.ts +17 -1
- package/app/composables/useResultView.ts +10 -21
- package/app/stores/github.ts +24 -0
- package/app/stores/pipelines.ts +9 -0
- package/app/utils/pipeline.ts +25 -1
- package/i18n/locales/de.json +20 -0
- package/i18n/locales/en.json +20 -0
- package/i18n/locales/es.json +20 -0
- package/i18n/locales/fr.json +20 -0
- package/i18n/locales/he.json +20 -0
- package/i18n/locales/it.json +20 -0
- package/i18n/locales/ja.json +20 -0
- package/i18n/locales/pl.json +20 -0
- package/i18n/locales/tr.json +20 -0
- package/i18n/locales/uk.json +20 -0
- package/package.json +2 -2
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
RequirementPriority,
|
|
13
13
|
SpecModule,
|
|
14
14
|
} from '~/types/spec'
|
|
15
|
-
import
|
|
15
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
16
16
|
|
|
17
17
|
const { t } = useI18n()
|
|
18
18
|
const board = useBoardStore()
|
|
@@ -121,291 +121,260 @@ function kindLabel(item: RequirementItem): string {
|
|
|
121
121
|
</script>
|
|
122
122
|
|
|
123
123
|
<template>
|
|
124
|
-
<
|
|
124
|
+
<ResultWindowShell
|
|
125
|
+
:open="open"
|
|
126
|
+
icon="i-lucide-scroll-text"
|
|
127
|
+
icon-class="bg-indigo-500/15 text-indigo-300"
|
|
128
|
+
:title="t('spec.title')"
|
|
129
|
+
:subtitle="block ? spec?.service || block.title : undefined"
|
|
130
|
+
variant="centered"
|
|
131
|
+
width="5xl"
|
|
132
|
+
@close="close"
|
|
133
|
+
>
|
|
134
|
+
<!-- view toggle: Gherkin only when the spec (and its feature files) are on main -->
|
|
135
|
+
<template v-if="present" #header-extras>
|
|
136
|
+
<div class="flex items-center rounded-lg border border-slate-700 p-0.5">
|
|
137
|
+
<UButton
|
|
138
|
+
:color="mode === 'structured' ? 'primary' : 'neutral'"
|
|
139
|
+
:variant="mode === 'structured' ? 'soft' : 'ghost'"
|
|
140
|
+
size="xs"
|
|
141
|
+
icon="i-lucide-list-tree"
|
|
142
|
+
@click="
|
|
143
|
+
() => {
|
|
144
|
+
mode = 'structured'
|
|
145
|
+
}
|
|
146
|
+
"
|
|
147
|
+
>
|
|
148
|
+
{{ t('spec.mode.structured') }}
|
|
149
|
+
</UButton>
|
|
150
|
+
<UButton
|
|
151
|
+
:color="mode === 'gherkin' ? 'primary' : 'neutral'"
|
|
152
|
+
:variant="mode === 'gherkin' ? 'soft' : 'ghost'"
|
|
153
|
+
size="xs"
|
|
154
|
+
icon="i-lucide-square-check-big"
|
|
155
|
+
:disabled="!hasGherkin"
|
|
156
|
+
:title="hasGherkin ? t('spec.mode.gherkinTooltip') : t('spec.mode.gherkinNone')"
|
|
157
|
+
@click="
|
|
158
|
+
() => {
|
|
159
|
+
mode = 'gherkin'
|
|
160
|
+
}
|
|
161
|
+
"
|
|
162
|
+
>
|
|
163
|
+
{{ t('spec.mode.gherkin') }}
|
|
164
|
+
</UButton>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
<!-- loading -->
|
|
125
169
|
<div
|
|
126
|
-
v-if="
|
|
127
|
-
class="
|
|
128
|
-
@click.self="close"
|
|
170
|
+
v-if="loading && !view"
|
|
171
|
+
class="flex flex-1 items-center justify-center gap-2 text-sm text-slate-400"
|
|
129
172
|
>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
173
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
174
|
+
{{ t('spec.loading') }}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- error -->
|
|
178
|
+
<div
|
|
179
|
+
v-else-if="errored"
|
|
180
|
+
class="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center text-sm text-slate-400"
|
|
181
|
+
>
|
|
182
|
+
<UIcon name="i-lucide-triangle-alert" class="h-6 w-6 text-amber-400" />
|
|
183
|
+
{{ t('spec.error') }}
|
|
184
|
+
<UButton
|
|
185
|
+
icon="i-lucide-rotate-cw"
|
|
186
|
+
color="neutral"
|
|
187
|
+
variant="soft"
|
|
188
|
+
size="xs"
|
|
189
|
+
:loading="loading"
|
|
190
|
+
@click="retry"
|
|
134
191
|
>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
192
|
+
{{ t('common.retry') }}
|
|
193
|
+
</UButton>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- empty: no spec on the repo's default branch yet -->
|
|
197
|
+
<div
|
|
198
|
+
v-else-if="!present"
|
|
199
|
+
class="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center"
|
|
200
|
+
>
|
|
201
|
+
<UIcon name="i-lucide-scroll-text" class="h-8 w-8 text-slate-600" />
|
|
202
|
+
<div>
|
|
203
|
+
<p class="text-sm font-medium text-slate-300">{{ t('spec.empty.title') }}</p>
|
|
204
|
+
<p class="mx-auto mt-1 max-w-md text-xs text-slate-500">
|
|
205
|
+
{{ t('spec.empty.description') }}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- spec body: navigable tree + detail -->
|
|
211
|
+
<div v-else class="flex min-h-0 flex-1">
|
|
212
|
+
<!-- nav: modules → feature groups -->
|
|
213
|
+
<nav class="w-64 shrink-0 overflow-y-auto border-e border-slate-800 px-3 py-4">
|
|
214
|
+
<UButton
|
|
215
|
+
block
|
|
216
|
+
class="mb-2 justify-start"
|
|
217
|
+
:color="selected === null ? 'primary' : 'neutral'"
|
|
218
|
+
:variant="selected === null ? 'soft' : 'ghost'"
|
|
219
|
+
size="xs"
|
|
220
|
+
icon="i-lucide-info"
|
|
221
|
+
@click="
|
|
222
|
+
() => {
|
|
223
|
+
selected = null
|
|
224
|
+
}
|
|
225
|
+
"
|
|
226
|
+
>
|
|
227
|
+
{{ t('spec.overview') }}
|
|
228
|
+
</UButton>
|
|
229
|
+
<div v-for="(mod, mi) in modules" :key="mi" class="mb-3">
|
|
230
|
+
<div class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
231
|
+
{{ mod.name }}
|
|
147
232
|
</div>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
() => {
|
|
158
|
-
mode = 'structured'
|
|
159
|
-
}
|
|
233
|
+
<ul class="space-y-0.5">
|
|
234
|
+
<li v-for="(group, gi) in mod.groups ?? []" :key="gi">
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
class="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-start text-[13px] transition"
|
|
238
|
+
:class="
|
|
239
|
+
selected?.m === mi && selected?.g === gi
|
|
240
|
+
? 'bg-indigo-500/15 text-indigo-200'
|
|
241
|
+
: 'text-slate-300 hover:bg-slate-800'
|
|
160
242
|
"
|
|
243
|
+
@click="selectGroup(mi, gi)"
|
|
161
244
|
>
|
|
162
|
-
{{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
mode = 'gherkin'
|
|
174
|
-
}
|
|
175
|
-
"
|
|
176
|
-
>
|
|
177
|
-
{{ t('spec.mode.gherkin') }}
|
|
178
|
-
</UButton>
|
|
179
|
-
</div>
|
|
180
|
-
<IconButton
|
|
181
|
-
icon="i-lucide-x"
|
|
182
|
-
color="neutral"
|
|
183
|
-
variant="ghost"
|
|
184
|
-
size="sm"
|
|
185
|
-
:label="t('common.close')"
|
|
186
|
-
@click="close"
|
|
187
|
-
/>
|
|
188
|
-
</div>
|
|
189
|
-
</header>
|
|
190
|
-
|
|
191
|
-
<!-- loading -->
|
|
192
|
-
<div
|
|
193
|
-
v-if="loading && !view"
|
|
194
|
-
class="flex flex-1 items-center justify-center gap-2 text-sm text-slate-400"
|
|
195
|
-
>
|
|
196
|
-
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
197
|
-
{{ t('spec.loading') }}
|
|
245
|
+
<span class="truncate">{{ group.name }}</span>
|
|
246
|
+
<span class="shrink-0 text-[10px] text-slate-500">{{ reqCount(group) }}</span>
|
|
247
|
+
</button>
|
|
248
|
+
</li>
|
|
249
|
+
<li
|
|
250
|
+
v-if="(mod.groups?.length ?? 0) === 0"
|
|
251
|
+
class="px-2 py-1 text-[11px] italic text-slate-600"
|
|
252
|
+
>
|
|
253
|
+
{{ t('spec.noFeatureGroups') }}
|
|
254
|
+
</li>
|
|
255
|
+
</ul>
|
|
198
256
|
</div>
|
|
257
|
+
</nav>
|
|
199
258
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
</div>
|
|
259
|
+
<!-- detail -->
|
|
260
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
|
|
261
|
+
<!-- service overview -->
|
|
262
|
+
<template v-if="selected === null">
|
|
263
|
+
<h2 class="text-lg font-semibold text-white">{{ spec?.service }}</h2>
|
|
264
|
+
<p v-if="spec?.summary" class="mt-2 whitespace-pre-line text-sm text-slate-300">
|
|
265
|
+
{{ spec.summary }}
|
|
266
|
+
</p>
|
|
267
|
+
<p v-else class="mt-2 text-sm text-slate-500">{{ t('spec.noSummary') }}</p>
|
|
268
|
+
<p class="mt-4 text-xs text-slate-500">
|
|
269
|
+
{{
|
|
270
|
+
hasGherkin
|
|
271
|
+
? t('spec.moduleHintGherkin', { count: modules.length }, modules.length)
|
|
272
|
+
: t('spec.moduleHint', { count: modules.length }, modules.length)
|
|
273
|
+
}}
|
|
274
|
+
</p>
|
|
275
|
+
</template>
|
|
218
276
|
|
|
219
|
-
<!--
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
>
|
|
224
|
-
<UIcon name="i-lucide-scroll-text" class="h-8 w-8 text-slate-600" />
|
|
225
|
-
<div>
|
|
226
|
-
<p class="text-sm font-medium text-slate-300">{{ t('spec.empty.title') }}</p>
|
|
227
|
-
<p class="mx-auto mt-1 max-w-md text-xs text-slate-500">
|
|
228
|
-
{{ t('spec.empty.description') }}
|
|
229
|
-
</p>
|
|
277
|
+
<!-- selected feature group -->
|
|
278
|
+
<template v-else-if="selectedGroup">
|
|
279
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
280
|
+
{{ selectedModule?.name }}
|
|
230
281
|
</div>
|
|
231
|
-
|
|
282
|
+
<h2 class="text-lg font-semibold text-white">{{ selectedGroup.name }}</h2>
|
|
283
|
+
<p v-if="selectedGroup.summary" class="mt-1 text-sm text-slate-400">
|
|
284
|
+
{{ selectedGroup.summary }}
|
|
285
|
+
</p>
|
|
232
286
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
size="xs"
|
|
243
|
-
icon="i-lucide-info"
|
|
244
|
-
@click="
|
|
245
|
-
() => {
|
|
246
|
-
selected = null
|
|
247
|
-
}
|
|
248
|
-
"
|
|
287
|
+
<!-- GHERKIN view: the rendered .feature file for this group -->
|
|
288
|
+
<template v-if="mode === 'gherkin'">
|
|
289
|
+
<pre
|
|
290
|
+
v-if="selectedFeature"
|
|
291
|
+
class="mt-4 overflow-x-auto rounded-lg border border-slate-800 bg-slate-950/60 p-4 text-[12.5px] leading-relaxed text-slate-200"
|
|
292
|
+
><code>{{ selectedFeature.content }}</code></pre>
|
|
293
|
+
<div
|
|
294
|
+
v-else
|
|
295
|
+
class="mt-4 rounded-lg border border-dashed border-slate-700 p-6 text-center text-sm text-slate-500"
|
|
249
296
|
>
|
|
250
|
-
{{ t('spec.
|
|
251
|
-
</
|
|
252
|
-
|
|
297
|
+
{{ t('spec.noGherkinForGroup') }}
|
|
298
|
+
</div>
|
|
299
|
+
</template>
|
|
300
|
+
|
|
301
|
+
<!-- STRUCTURED view: requirements + acceptance + domain rules -->
|
|
302
|
+
<template v-else>
|
|
303
|
+
<div v-if="reqCount(selectedGroup) === 0" class="mt-4 text-sm text-slate-500">
|
|
304
|
+
{{ t('spec.noRequirements') }}
|
|
305
|
+
</div>
|
|
306
|
+
<ul class="mt-4 space-y-4">
|
|
307
|
+
<li
|
|
308
|
+
v-for="req in selectedGroup.requirements ?? []"
|
|
309
|
+
:key="req.id"
|
|
310
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
311
|
+
>
|
|
312
|
+
<div class="flex items-start justify-between gap-3">
|
|
313
|
+
<h3 class="text-sm font-semibold text-slate-100">{{ req.title }}</h3>
|
|
314
|
+
<div class="flex shrink-0 items-center gap-1.5">
|
|
315
|
+
<UBadge :color="priorityMeta(req).chip as any" variant="subtle" size="sm">
|
|
316
|
+
{{ priorityMeta(req).label }}
|
|
317
|
+
</UBadge>
|
|
318
|
+
<UBadge color="neutral" variant="subtle" size="sm">{{ kindLabel(req) }}</UBadge>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
<p class="mt-1.5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300">
|
|
322
|
+
{{ req.statement }}
|
|
323
|
+
</p>
|
|
324
|
+
<!-- acceptance criteria (Given/When/Then) -->
|
|
325
|
+
<div v-if="(req.acceptance?.length ?? 0) > 0" class="mt-3 space-y-1.5">
|
|
326
|
+
<div
|
|
327
|
+
v-for="ac in req.acceptance ?? []"
|
|
328
|
+
:key="ac.id"
|
|
329
|
+
class="rounded-md border border-slate-800 bg-slate-950/50 px-3 py-2 text-[12.5px] leading-relaxed"
|
|
330
|
+
>
|
|
331
|
+
<p class="text-slate-300">
|
|
332
|
+
<span class="font-semibold text-emerald-400">{{
|
|
333
|
+
t('spec.acceptance.given')
|
|
334
|
+
}}</span>
|
|
335
|
+
{{ ac.given }}
|
|
336
|
+
</p>
|
|
337
|
+
<p class="text-slate-300">
|
|
338
|
+
<span class="font-semibold text-sky-400">{{
|
|
339
|
+
t('spec.acceptance.when')
|
|
340
|
+
}}</span>
|
|
341
|
+
{{ ac.when }}
|
|
342
|
+
</p>
|
|
343
|
+
<p class="text-slate-300">
|
|
344
|
+
<span class="font-semibold text-violet-400">{{
|
|
345
|
+
t('spec.acceptance.then')
|
|
346
|
+
}}</span>
|
|
347
|
+
{{ ac.outcome }}
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</li>
|
|
352
|
+
</ul>
|
|
353
|
+
|
|
354
|
+
<!-- domain rules / invariants scoped to this group -->
|
|
355
|
+
<div v-if="(selectedGroup.rules?.length ?? 0) > 0" class="mt-6">
|
|
253
356
|
<div
|
|
254
|
-
class="
|
|
357
|
+
class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
255
358
|
>
|
|
256
|
-
|
|
359
|
+
<UIcon name="i-lucide-shield-check" class="h-3.5 w-3.5" />
|
|
360
|
+
{{ t('spec.domainRules') }}
|
|
257
361
|
</div>
|
|
258
|
-
<ul class="space-y-
|
|
259
|
-
<li v-for="(group, gi) in mod.groups ?? []" :key="gi">
|
|
260
|
-
<button
|
|
261
|
-
type="button"
|
|
262
|
-
class="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-start text-[13px] transition"
|
|
263
|
-
:class="
|
|
264
|
-
selected?.m === mi && selected?.g === gi
|
|
265
|
-
? 'bg-indigo-500/15 text-indigo-200'
|
|
266
|
-
: 'text-slate-300 hover:bg-slate-800'
|
|
267
|
-
"
|
|
268
|
-
@click="selectGroup(mi, gi)"
|
|
269
|
-
>
|
|
270
|
-
<span class="truncate">{{ group.name }}</span>
|
|
271
|
-
<span class="shrink-0 text-[10px] text-slate-500">{{ reqCount(group) }}</span>
|
|
272
|
-
</button>
|
|
273
|
-
</li>
|
|
362
|
+
<ul class="space-y-1.5">
|
|
274
363
|
<li
|
|
275
|
-
v-
|
|
276
|
-
|
|
364
|
+
v-for="rule in selectedGroup.rules ?? []"
|
|
365
|
+
:key="rule.id"
|
|
366
|
+
class="rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2 text-[13px] text-slate-300"
|
|
277
367
|
>
|
|
278
|
-
{{
|
|
368
|
+
{{ rule.rule }}
|
|
369
|
+
<span v-if="rule.rationale" class="text-slate-500">{{
|
|
370
|
+
t('spec.ruleRationale', { rationale: rule.rationale })
|
|
371
|
+
}}</span>
|
|
279
372
|
</li>
|
|
280
373
|
</ul>
|
|
281
374
|
</div>
|
|
282
|
-
</
|
|
283
|
-
|
|
284
|
-
<!-- detail -->
|
|
285
|
-
<div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
|
|
286
|
-
<!-- service overview -->
|
|
287
|
-
<template v-if="selected === null">
|
|
288
|
-
<h2 class="text-lg font-semibold text-white">{{ spec?.service }}</h2>
|
|
289
|
-
<p v-if="spec?.summary" class="mt-2 whitespace-pre-line text-sm text-slate-300">
|
|
290
|
-
{{ spec.summary }}
|
|
291
|
-
</p>
|
|
292
|
-
<p v-else class="mt-2 text-sm text-slate-500">{{ t('spec.noSummary') }}</p>
|
|
293
|
-
<p class="mt-4 text-xs text-slate-500">
|
|
294
|
-
{{
|
|
295
|
-
hasGherkin
|
|
296
|
-
? t('spec.moduleHintGherkin', { count: modules.length }, modules.length)
|
|
297
|
-
: t('spec.moduleHint', { count: modules.length }, modules.length)
|
|
298
|
-
}}
|
|
299
|
-
</p>
|
|
300
|
-
</template>
|
|
301
|
-
|
|
302
|
-
<!-- selected feature group -->
|
|
303
|
-
<template v-else-if="selectedGroup">
|
|
304
|
-
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
305
|
-
{{ selectedModule?.name }}
|
|
306
|
-
</div>
|
|
307
|
-
<h2 class="text-lg font-semibold text-white">{{ selectedGroup.name }}</h2>
|
|
308
|
-
<p v-if="selectedGroup.summary" class="mt-1 text-sm text-slate-400">
|
|
309
|
-
{{ selectedGroup.summary }}
|
|
310
|
-
</p>
|
|
311
|
-
|
|
312
|
-
<!-- GHERKIN view: the rendered .feature file for this group -->
|
|
313
|
-
<template v-if="mode === 'gherkin'">
|
|
314
|
-
<pre
|
|
315
|
-
v-if="selectedFeature"
|
|
316
|
-
class="mt-4 overflow-x-auto rounded-lg border border-slate-800 bg-slate-950/60 p-4 text-[12.5px] leading-relaxed text-slate-200"
|
|
317
|
-
><code>{{ selectedFeature.content }}</code></pre>
|
|
318
|
-
<div
|
|
319
|
-
v-else
|
|
320
|
-
class="mt-4 rounded-lg border border-dashed border-slate-700 p-6 text-center text-sm text-slate-500"
|
|
321
|
-
>
|
|
322
|
-
{{ t('spec.noGherkinForGroup') }}
|
|
323
|
-
</div>
|
|
324
|
-
</template>
|
|
325
|
-
|
|
326
|
-
<!-- STRUCTURED view: requirements + acceptance + domain rules -->
|
|
327
|
-
<template v-else>
|
|
328
|
-
<div v-if="reqCount(selectedGroup) === 0" class="mt-4 text-sm text-slate-500">
|
|
329
|
-
{{ t('spec.noRequirements') }}
|
|
330
|
-
</div>
|
|
331
|
-
<ul class="mt-4 space-y-4">
|
|
332
|
-
<li
|
|
333
|
-
v-for="req in selectedGroup.requirements ?? []"
|
|
334
|
-
:key="req.id"
|
|
335
|
-
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
336
|
-
>
|
|
337
|
-
<div class="flex items-start justify-between gap-3">
|
|
338
|
-
<h3 class="text-sm font-semibold text-slate-100">{{ req.title }}</h3>
|
|
339
|
-
<div class="flex shrink-0 items-center gap-1.5">
|
|
340
|
-
<UBadge :color="priorityMeta(req).chip as any" variant="subtle" size="sm">
|
|
341
|
-
{{ priorityMeta(req).label }}
|
|
342
|
-
</UBadge>
|
|
343
|
-
<UBadge color="neutral" variant="subtle" size="sm">{{
|
|
344
|
-
kindLabel(req)
|
|
345
|
-
}}</UBadge>
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
<p
|
|
349
|
-
class="mt-1.5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300"
|
|
350
|
-
>
|
|
351
|
-
{{ req.statement }}
|
|
352
|
-
</p>
|
|
353
|
-
<!-- acceptance criteria (Given/When/Then) -->
|
|
354
|
-
<div v-if="(req.acceptance?.length ?? 0) > 0" class="mt-3 space-y-1.5">
|
|
355
|
-
<div
|
|
356
|
-
v-for="ac in req.acceptance ?? []"
|
|
357
|
-
:key="ac.id"
|
|
358
|
-
class="rounded-md border border-slate-800 bg-slate-950/50 px-3 py-2 text-[12.5px] leading-relaxed"
|
|
359
|
-
>
|
|
360
|
-
<p class="text-slate-300">
|
|
361
|
-
<span class="font-semibold text-emerald-400">{{
|
|
362
|
-
t('spec.acceptance.given')
|
|
363
|
-
}}</span>
|
|
364
|
-
{{ ac.given }}
|
|
365
|
-
</p>
|
|
366
|
-
<p class="text-slate-300">
|
|
367
|
-
<span class="font-semibold text-sky-400">{{
|
|
368
|
-
t('spec.acceptance.when')
|
|
369
|
-
}}</span>
|
|
370
|
-
{{ ac.when }}
|
|
371
|
-
</p>
|
|
372
|
-
<p class="text-slate-300">
|
|
373
|
-
<span class="font-semibold text-violet-400">{{
|
|
374
|
-
t('spec.acceptance.then')
|
|
375
|
-
}}</span>
|
|
376
|
-
{{ ac.outcome }}
|
|
377
|
-
</p>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
</li>
|
|
381
|
-
</ul>
|
|
382
|
-
|
|
383
|
-
<!-- domain rules / invariants scoped to this group -->
|
|
384
|
-
<div v-if="(selectedGroup.rules?.length ?? 0) > 0" class="mt-6">
|
|
385
|
-
<div
|
|
386
|
-
class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
387
|
-
>
|
|
388
|
-
<UIcon name="i-lucide-shield-check" class="h-3.5 w-3.5" />
|
|
389
|
-
{{ t('spec.domainRules') }}
|
|
390
|
-
</div>
|
|
391
|
-
<ul class="space-y-1.5">
|
|
392
|
-
<li
|
|
393
|
-
v-for="rule in selectedGroup.rules ?? []"
|
|
394
|
-
:key="rule.id"
|
|
395
|
-
class="rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2 text-[13px] text-slate-300"
|
|
396
|
-
>
|
|
397
|
-
{{ rule.rule }}
|
|
398
|
-
<span v-if="rule.rationale" class="text-slate-500">{{
|
|
399
|
-
t('spec.ruleRationale', { rationale: rule.rationale })
|
|
400
|
-
}}</span>
|
|
401
|
-
</li>
|
|
402
|
-
</ul>
|
|
403
|
-
</div>
|
|
404
|
-
</template>
|
|
405
|
-
</template>
|
|
406
|
-
</div>
|
|
407
|
-
</div>
|
|
375
|
+
</template>
|
|
376
|
+
</template>
|
|
408
377
|
</div>
|
|
409
378
|
</div>
|
|
410
|
-
</
|
|
379
|
+
</ResultWindowShell>
|
|
411
380
|
</template>
|
|
@@ -30,12 +30,10 @@ const blobs = useArtifactBlobs()
|
|
|
30
30
|
onUnmounted(() => blobs.revokeAll())
|
|
31
31
|
|
|
32
32
|
// Shared seam contract (open/blockId/close). No `onOpen` loader: this window reads its report
|
|
33
|
-
// straight off the execution step, so there's nothing to fetch on open. `
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
const { open, blockId, instanceId, stepIndex, close } = useResultView('tester'
|
|
37
|
-
manageEscape: false,
|
|
38
|
-
})
|
|
33
|
+
// straight off the execution step, so there's nothing to fetch on open. `ResultWindowShell`
|
|
34
|
+
// owns Escape (and the focus trap + scroll lock + stacking) via the shared overlay behaviour;
|
|
35
|
+
// the nested lightbox layers above it on the same stack.
|
|
36
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('tester')
|
|
39
37
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
40
38
|
const headerTitle = computed(
|
|
41
39
|
() => `${t('testing.title')}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
@@ -26,11 +26,9 @@ const access = useWorkspaceAccess()
|
|
|
26
26
|
const blobs = useArtifactBlobs()
|
|
27
27
|
onUnmounted(() => blobs.revokeAll())
|
|
28
28
|
|
|
29
|
-
// `
|
|
30
|
-
//
|
|
31
|
-
const { open, blockId, instanceId, stepIndex, close } = useResultView('visual-confirm'
|
|
32
|
-
manageEscape: false,
|
|
33
|
-
})
|
|
29
|
+
// `ResultWindowShell` owns Escape (and the focus trap + scroll lock + stacking); the nested
|
|
30
|
+
// lightbox layers above it on the same shared overlay stack.
|
|
31
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('visual-confirm')
|
|
34
32
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
35
33
|
const headerTitle = computed(() =>
|
|
36
34
|
block.value
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
listGitHubIssuesContract,
|
|
14
14
|
listGitHubPullsContract,
|
|
15
15
|
listGitHubReposContract,
|
|
16
|
+
listGitHubRepoFilesContract,
|
|
16
17
|
listGitHubRepoTreeContract,
|
|
17
18
|
mergeGitHubPullRequestContract,
|
|
18
19
|
openGitHubPullRequestContract,
|
|
@@ -92,6 +93,13 @@ export function githubApi({ send, ws }: ApiContext) {
|
|
|
92
93
|
queryParams: { path },
|
|
93
94
|
}),
|
|
94
95
|
|
|
96
|
+
// List every file in a repo (whole tree, one recursive read) for file-path search.
|
|
97
|
+
listGitHubRepoFiles: (workspaceId: string, repoGithubId: number) =>
|
|
98
|
+
send(listGitHubRepoFilesContract, {
|
|
99
|
+
pathPrefix: ws(workspaceId),
|
|
100
|
+
pathParams: { repoGithubId: String(repoGithubId) },
|
|
101
|
+
}),
|
|
102
|
+
|
|
95
103
|
listGitHubBranches: (workspaceId: string, repoGithubId: number) =>
|
|
96
104
|
send(listGitHubBranchesContract, {
|
|
97
105
|
pathPrefix: ws(workspaceId),
|