@cat-factory/app 0.92.2 → 0.94.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/auth/LoginScreen.vue +5 -1
- package/app/components/board/AddTaskModal.vue +25 -5
- package/app/components/board/CreateInitiativeModal.vue +9 -1
- package/app/components/board/RecurringPipelineModal.vue +20 -4
- package/app/components/bootstrap/BootstrapModal.vue +9 -1
- package/app/components/brainstorm/BrainstormWindow.vue +5 -1
- package/app/components/clarity/ClarityReviewWindow.vue +5 -1
- package/app/components/docs/DocInterviewWindow.vue +209 -0
- package/app/components/documents/DocumentTemplatesModal.vue +5 -1
- package/app/components/fragments/FragmentLibraryManager.vue +10 -2
- package/app/components/github/GitHubPanel.vue +25 -4
- package/app/components/layout/BoardSwitcher.vue +10 -1
- package/app/components/layout/BoardToolbar.vue +15 -3
- package/app/components/layout/GitHubPatBanner.vue +5 -1
- package/app/components/layout/ProviderConfigBanner.vue +5 -1
- package/app/components/panels/AgentStepDetail.vue +5 -1
- package/app/components/panels/InspectorPanel.vue +5 -1
- package/app/components/panels/StepRestartControl.vue +10 -2
- package/app/components/panels/StepResultViewHost.vue +4 -0
- package/app/components/panels/inspector/FrontendConfig.vue +10 -1
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +11 -3
- package/app/components/pipeline/PipelineBuilder.vue +15 -2
- package/app/components/pipeline/PipelineProgress.vue +10 -2
- package/app/components/providers/AiPresetMismatchDialog.vue +10 -1
- package/app/components/providers/PersonalCredentialModal.vue +18 -2
- package/app/components/requirements/RequirementsReviewWindow.vue +5 -1
- package/app/components/sandbox/SandboxPanel.vue +10 -1
- package/app/components/settings/InfraHandlersConfigurator.vue +5 -1
- package/app/components/settings/InfrastructureWindow.vue +18 -2
- package/app/components/settings/IssueTrackerPanel.vue +20 -4
- package/app/components/settings/LocalModelEndpointsPanel.vue +5 -1
- package/app/components/settings/ModelConfigurationPanel.vue +20 -3
- package/app/components/settings/ProviderConnectionTab.vue +5 -1
- package/app/components/settings/SharedStacksPanel.vue +363 -0
- package/app/components/spec/ServiceSpecWindow.vue +15 -3
- package/app/components/testing/TestReportWindow.vue +10 -2
- package/app/composables/api/docInterview.ts +36 -0
- package/app/composables/api/sharedStacks.ts +42 -0
- package/app/composables/useApi.ts +4 -0
- package/app/composables/useWorkspaceStream.ts +5 -0
- package/app/stores/docInterview.ts +89 -0
- package/app/stores/sharedStacks.ts +65 -0
- package/app/stores/workspace.ts +3 -0
- package/app/types/doc-interview.ts +9 -0
- package/app/types/domain.ts +1 -0
- package/app/types/sharedStacks.ts +8 -0
- package/i18n/locales/en.json +62 -0
- package/i18n/locales/es.json +62 -0
- package/i18n/locales/fr.json +62 -0
- package/i18n/locales/he.json +62 -0
- package/i18n/locales/ja.json +62 -0
- package/i18n/locales/pl.json +62 -0
- package/i18n/locales/tr.json +62 -0
- package/i18n/locales/uk.json +62 -0
- package/package.json +5 -5
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shared stacks — long-lived compose infra (e.g. acme-shared-services: MySQL / Postgres /
|
|
3
|
+
// Valkey / RabbitMQ / Kafka / ES / Mailpit / Envoy) brought up ONCE per workspace and reused
|
|
4
|
+
// across runs + PRs. A per-PR consumer environment attaches to a stack's managed network. CRUD
|
|
5
|
+
// works on every backend; the bring-up (Start) / teardown (Stop) drive a host Docker daemon, so
|
|
6
|
+
// they succeed only on the local facade (elsewhere the backend returns a clear error surfaced as
|
|
7
|
+
// a toast). Renders inline inside the Infrastructure window's "Shared stacks" tab.
|
|
8
|
+
import { computed, reactive, ref } from 'vue'
|
|
9
|
+
import type { SharedStack, SharedStackStatus } from '~/types/sharedStacks'
|
|
10
|
+
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
const store = useSharedStacksStore()
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
15
|
+
|
|
16
|
+
const stacks = computed(() => store.stacks)
|
|
17
|
+
const busyId = ref<string | null>(null)
|
|
18
|
+
const saving = ref(false)
|
|
19
|
+
// null ⇒ the form is in "add" mode; a stack id ⇒ editing that stack's definition in place.
|
|
20
|
+
const editingId = ref<string | null>(null)
|
|
21
|
+
|
|
22
|
+
const form = reactive({
|
|
23
|
+
name: '',
|
|
24
|
+
cloneUrl: '',
|
|
25
|
+
gitRef: '',
|
|
26
|
+
composeFiles: '',
|
|
27
|
+
composeProfiles: '',
|
|
28
|
+
managedNetworks: '',
|
|
29
|
+
allowHostCommands: false,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** Status → badge colour. */
|
|
33
|
+
const STATUS_COLOR: Record<SharedStackStatus, 'neutral' | 'warning' | 'success' | 'error'> = {
|
|
34
|
+
stopped: 'neutral',
|
|
35
|
+
starting: 'warning',
|
|
36
|
+
running: 'success',
|
|
37
|
+
failed: 'error',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Status → catalog key as an EXHAUSTIVE Record over the enum (not a runtime-assembled key), so a
|
|
41
|
+
// new SharedStackStatus fails the typecheck on this map instead of leaking a raw key into the badge.
|
|
42
|
+
const STATUS_LABEL_KEYS: Record<SharedStackStatus, string> = {
|
|
43
|
+
stopped: 'settings.sharedStacks.status.stopped',
|
|
44
|
+
starting: 'settings.sharedStacks.status.starting',
|
|
45
|
+
running: 'settings.sharedStacks.status.running',
|
|
46
|
+
failed: 'settings.sharedStacks.status.failed',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function statusLabel(status: SharedStackStatus): string {
|
|
50
|
+
return t(STATUS_LABEL_KEYS[status])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A running/starting stack cannot be reconfigured (the backend refuses) — edit is stopped/failed only. */
|
|
54
|
+
function canEdit(stack: SharedStack): boolean {
|
|
55
|
+
return stack.status !== 'running' && stack.status !== 'starting'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Split a comma/whitespace-separated field into trimmed non-empty tokens. */
|
|
59
|
+
function tokens(value: string): string[] {
|
|
60
|
+
return value
|
|
61
|
+
.split(/[\s,]+/)
|
|
62
|
+
.map((s) => s.trim())
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const canSave = computed(
|
|
67
|
+
() => form.name.trim() && form.cloneUrl.trim() && tokens(form.composeFiles).length > 0,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
function resetForm() {
|
|
71
|
+
editingId.value = null
|
|
72
|
+
form.name = ''
|
|
73
|
+
form.cloneUrl = ''
|
|
74
|
+
form.gitRef = ''
|
|
75
|
+
form.composeFiles = ''
|
|
76
|
+
form.composeProfiles = ''
|
|
77
|
+
form.managedNetworks = ''
|
|
78
|
+
form.allowHostCommands = false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Load a stack's definition into the form for in-place editing. */
|
|
82
|
+
function startEdit(stack: SharedStack) {
|
|
83
|
+
editingId.value = stack.id
|
|
84
|
+
form.name = stack.name
|
|
85
|
+
form.cloneUrl = stack.cloneUrl
|
|
86
|
+
form.gitRef = stack.gitRef ?? ''
|
|
87
|
+
form.composeFiles = stack.composeFiles.join(', ')
|
|
88
|
+
form.composeProfiles = stack.composeProfiles.join(', ')
|
|
89
|
+
form.managedNetworks = stack.managedNetworks.join(', ')
|
|
90
|
+
form.allowHostCommands = stack.allowHostCommands
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function notifyError(title: string, e: unknown) {
|
|
94
|
+
toast.add({
|
|
95
|
+
title,
|
|
96
|
+
description: e instanceof Error ? e.message : String(e),
|
|
97
|
+
icon: 'i-lucide-triangle-alert',
|
|
98
|
+
color: 'error',
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Create a new stack, or save edits to the one being edited (same form, mode toggled by `editingId`). */
|
|
103
|
+
async function saveStack() {
|
|
104
|
+
saving.value = true
|
|
105
|
+
const editing = editingId.value
|
|
106
|
+
const payload = {
|
|
107
|
+
name: form.name.trim(),
|
|
108
|
+
cloneUrl: form.cloneUrl.trim(),
|
|
109
|
+
...(form.gitRef.trim() ? { gitRef: form.gitRef.trim() } : {}),
|
|
110
|
+
composeFiles: tokens(form.composeFiles),
|
|
111
|
+
composeProfiles: tokens(form.composeProfiles),
|
|
112
|
+
managedNetworks: tokens(form.managedNetworks),
|
|
113
|
+
allowHostCommands: form.allowHostCommands,
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
if (editing) {
|
|
117
|
+
await store.update(editing, { ...payload, gitRef: form.gitRef.trim() || null })
|
|
118
|
+
} else {
|
|
119
|
+
await store.create(payload)
|
|
120
|
+
}
|
|
121
|
+
resetForm()
|
|
122
|
+
toast.add({
|
|
123
|
+
title: t(
|
|
124
|
+
editing ? 'settings.sharedStacks.toast.updated' : 'settings.sharedStacks.toast.created',
|
|
125
|
+
),
|
|
126
|
+
icon: 'i-lucide-check',
|
|
127
|
+
color: 'success',
|
|
128
|
+
})
|
|
129
|
+
} catch (e) {
|
|
130
|
+
notifyError(
|
|
131
|
+
t(
|
|
132
|
+
editing
|
|
133
|
+
? 'settings.sharedStacks.toast.updateFailed'
|
|
134
|
+
: 'settings.sharedStacks.toast.createFailed',
|
|
135
|
+
),
|
|
136
|
+
e,
|
|
137
|
+
)
|
|
138
|
+
} finally {
|
|
139
|
+
saving.value = false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function start(stack: SharedStack) {
|
|
144
|
+
busyId.value = stack.id
|
|
145
|
+
try {
|
|
146
|
+
// ensureUp resolves 200 even on a FAILED bring-up (the record carries status/lastError), so
|
|
147
|
+
// surface that as an error toast too — not only a thrown transport/unavailable error.
|
|
148
|
+
const updated = await store.ensureUp(stack.id)
|
|
149
|
+
if (updated.status === 'failed') {
|
|
150
|
+
notifyError(t('settings.sharedStacks.toast.startFailed'), updated.lastError ?? '')
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
notifyError(t('settings.sharedStacks.toast.startFailed'), e)
|
|
154
|
+
} finally {
|
|
155
|
+
busyId.value = null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function stop(stack: SharedStack) {
|
|
160
|
+
busyId.value = stack.id
|
|
161
|
+
try {
|
|
162
|
+
await store.teardown(stack.id)
|
|
163
|
+
} catch (e) {
|
|
164
|
+
notifyError(t('settings.sharedStacks.toast.stopFailed'), e)
|
|
165
|
+
} finally {
|
|
166
|
+
busyId.value = null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function remove(stack: SharedStack) {
|
|
171
|
+
const noun = t('settings.sharedStacks.stackNoun')
|
|
172
|
+
if (!(await confirmAction('remove', noun))) return
|
|
173
|
+
busyId.value = stack.id
|
|
174
|
+
try {
|
|
175
|
+
await store.remove(stack.id)
|
|
176
|
+
toastDone('remove', noun)
|
|
177
|
+
} catch (e) {
|
|
178
|
+
notifyError(t('settings.sharedStacks.toast.removeFailed'), e)
|
|
179
|
+
} finally {
|
|
180
|
+
busyId.value = null
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<template>
|
|
186
|
+
<div class="space-y-4" data-testid="shared-stacks-panel">
|
|
187
|
+
<p class="text-sm text-slate-400">{{ t('settings.sharedStacks.intro') }}</p>
|
|
188
|
+
|
|
189
|
+
<section v-if="stacks.length" class="space-y-2 rounded-lg border border-slate-700 p-3">
|
|
190
|
+
<h3 class="text-sm font-semibold">{{ t('settings.sharedStacks.list.heading') }}</h3>
|
|
191
|
+
<div
|
|
192
|
+
v-for="stack in stacks"
|
|
193
|
+
:key="stack.id"
|
|
194
|
+
class="space-y-2 rounded-md border border-slate-800 px-3 py-2"
|
|
195
|
+
:data-testid="`shared-stack-${stack.id}`"
|
|
196
|
+
>
|
|
197
|
+
<div class="flex items-center justify-between gap-2">
|
|
198
|
+
<div class="min-w-0 space-y-1">
|
|
199
|
+
<div class="flex items-center gap-2">
|
|
200
|
+
<span class="text-sm font-medium">{{ stack.name }}</span>
|
|
201
|
+
<UBadge :color="STATUS_COLOR[stack.status]" variant="soft" size="sm">
|
|
202
|
+
{{ statusLabel(stack.status) }}
|
|
203
|
+
</UBadge>
|
|
204
|
+
</div>
|
|
205
|
+
<p class="truncate text-[11px] text-slate-500">{{ stack.cloneUrl }}</p>
|
|
206
|
+
<div v-if="stack.managedNetworks.length" class="flex flex-wrap gap-1">
|
|
207
|
+
<UBadge
|
|
208
|
+
v-for="net in stack.managedNetworks"
|
|
209
|
+
:key="net"
|
|
210
|
+
color="neutral"
|
|
211
|
+
variant="soft"
|
|
212
|
+
size="sm"
|
|
213
|
+
>
|
|
214
|
+
{{ net }}
|
|
215
|
+
</UBadge>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
219
|
+
<UButton
|
|
220
|
+
v-if="stack.status !== 'running'"
|
|
221
|
+
icon="i-lucide-play"
|
|
222
|
+
size="sm"
|
|
223
|
+
variant="soft"
|
|
224
|
+
:loading="busyId === stack.id"
|
|
225
|
+
:data-testid="`shared-stack-start-${stack.id}`"
|
|
226
|
+
@click="start(stack)"
|
|
227
|
+
>
|
|
228
|
+
{{ t('settings.sharedStacks.list.start') }}
|
|
229
|
+
</UButton>
|
|
230
|
+
<UButton
|
|
231
|
+
v-else
|
|
232
|
+
icon="i-lucide-square"
|
|
233
|
+
size="sm"
|
|
234
|
+
variant="soft"
|
|
235
|
+
color="warning"
|
|
236
|
+
:loading="busyId === stack.id"
|
|
237
|
+
:data-testid="`shared-stack-stop-${stack.id}`"
|
|
238
|
+
@click="stop(stack)"
|
|
239
|
+
>
|
|
240
|
+
{{ t('settings.sharedStacks.list.stop') }}
|
|
241
|
+
</UButton>
|
|
242
|
+
<UButton
|
|
243
|
+
v-if="canEdit(stack)"
|
|
244
|
+
color="neutral"
|
|
245
|
+
variant="ghost"
|
|
246
|
+
icon="i-lucide-pencil"
|
|
247
|
+
size="sm"
|
|
248
|
+
:data-testid="`shared-stack-edit-${stack.id}`"
|
|
249
|
+
:aria-label="t('settings.sharedStacks.list.edit')"
|
|
250
|
+
@click="startEdit(stack)"
|
|
251
|
+
/>
|
|
252
|
+
<UButton
|
|
253
|
+
color="error"
|
|
254
|
+
variant="ghost"
|
|
255
|
+
icon="i-lucide-trash-2"
|
|
256
|
+
size="sm"
|
|
257
|
+
:loading="busyId === stack.id"
|
|
258
|
+
:data-testid="`shared-stack-delete-${stack.id}`"
|
|
259
|
+
:aria-label="t('settings.sharedStacks.list.remove')"
|
|
260
|
+
@click="remove(stack)"
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
<p v-if="stack.lastError" class="text-[11px] text-rose-400">{{ stack.lastError }}</p>
|
|
265
|
+
</div>
|
|
266
|
+
</section>
|
|
267
|
+
|
|
268
|
+
<section
|
|
269
|
+
class="space-y-3 rounded-lg border border-slate-700 p-3"
|
|
270
|
+
data-testid="shared-stack-form"
|
|
271
|
+
>
|
|
272
|
+
<h3 class="text-sm font-semibold">
|
|
273
|
+
{{
|
|
274
|
+
t(editingId ? 'settings.sharedStacks.edit.heading' : 'settings.sharedStacks.add.heading')
|
|
275
|
+
}}
|
|
276
|
+
</h3>
|
|
277
|
+
|
|
278
|
+
<UFormField :label="t('settings.sharedStacks.add.name')">
|
|
279
|
+
<UInput v-model="form.name" class="w-full" data-testid="shared-stack-name" />
|
|
280
|
+
</UFormField>
|
|
281
|
+
|
|
282
|
+
<UFormField
|
|
283
|
+
:label="t('settings.sharedStacks.add.cloneUrl')"
|
|
284
|
+
:help="t('settings.sharedStacks.add.cloneUrlHelp')"
|
|
285
|
+
>
|
|
286
|
+
<UInput
|
|
287
|
+
v-model="form.cloneUrl"
|
|
288
|
+
placeholder="https://github.com/acme/acme-shared-services.git"
|
|
289
|
+
class="w-full"
|
|
290
|
+
data-testid="shared-stack-clone-url"
|
|
291
|
+
/>
|
|
292
|
+
</UFormField>
|
|
293
|
+
|
|
294
|
+
<UFormField :label="t('settings.sharedStacks.add.gitRef')">
|
|
295
|
+
<UInput
|
|
296
|
+
v-model="form.gitRef"
|
|
297
|
+
placeholder="main"
|
|
298
|
+
class="w-full"
|
|
299
|
+
data-testid="shared-stack-git-ref"
|
|
300
|
+
/>
|
|
301
|
+
</UFormField>
|
|
302
|
+
|
|
303
|
+
<UFormField
|
|
304
|
+
:label="t('settings.sharedStacks.add.composeFiles')"
|
|
305
|
+
:help="t('settings.sharedStacks.add.composeFilesHelp')"
|
|
306
|
+
>
|
|
307
|
+
<UInput
|
|
308
|
+
v-model="form.composeFiles"
|
|
309
|
+
placeholder="docker-compose.yml, docker-compose.override.yml"
|
|
310
|
+
class="w-full"
|
|
311
|
+
data-testid="shared-stack-compose-files"
|
|
312
|
+
/>
|
|
313
|
+
</UFormField>
|
|
314
|
+
|
|
315
|
+
<UFormField :label="t('settings.sharedStacks.add.composeProfiles')">
|
|
316
|
+
<UInput
|
|
317
|
+
v-model="form.composeProfiles"
|
|
318
|
+
placeholder="backends, peer"
|
|
319
|
+
class="w-full"
|
|
320
|
+
data-testid="shared-stack-profiles"
|
|
321
|
+
/>
|
|
322
|
+
</UFormField>
|
|
323
|
+
|
|
324
|
+
<UFormField
|
|
325
|
+
:label="t('settings.sharedStacks.add.managedNetworks')"
|
|
326
|
+
:help="t('settings.sharedStacks.add.managedNetworksHelp')"
|
|
327
|
+
>
|
|
328
|
+
<UInput
|
|
329
|
+
v-model="form.managedNetworks"
|
|
330
|
+
placeholder="acme-net"
|
|
331
|
+
class="w-full"
|
|
332
|
+
data-testid="shared-stack-networks"
|
|
333
|
+
/>
|
|
334
|
+
</UFormField>
|
|
335
|
+
|
|
336
|
+
<UCheckbox
|
|
337
|
+
v-model="form.allowHostCommands"
|
|
338
|
+
:label="t('settings.sharedStacks.add.allowHostCommands')"
|
|
339
|
+
data-testid="shared-stack-allow-host-commands"
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
<div class="flex items-center gap-2">
|
|
343
|
+
<UButton
|
|
344
|
+
:loading="saving"
|
|
345
|
+
:disabled="!canSave"
|
|
346
|
+
data-testid="shared-stack-save"
|
|
347
|
+
@click="saveStack"
|
|
348
|
+
>
|
|
349
|
+
{{ t(editingId ? 'settings.sharedStacks.edit.save' : 'settings.sharedStacks.add.save') }}
|
|
350
|
+
</UButton>
|
|
351
|
+
<UButton
|
|
352
|
+
v-if="editingId"
|
|
353
|
+
color="neutral"
|
|
354
|
+
variant="ghost"
|
|
355
|
+
data-testid="shared-stack-cancel-edit"
|
|
356
|
+
@click="resetForm"
|
|
357
|
+
>
|
|
358
|
+
{{ t('settings.sharedStacks.edit.cancel') }}
|
|
359
|
+
</UButton>
|
|
360
|
+
</div>
|
|
361
|
+
</section>
|
|
362
|
+
</div>
|
|
363
|
+
</template>
|
|
@@ -147,7 +147,11 @@ function kindLabel(item: RequirementItem): string {
|
|
|
147
147
|
:variant="mode === 'structured' ? 'soft' : 'ghost'"
|
|
148
148
|
size="xs"
|
|
149
149
|
icon="i-lucide-list-tree"
|
|
150
|
-
@click="
|
|
150
|
+
@click="
|
|
151
|
+
() => {
|
|
152
|
+
mode = 'structured'
|
|
153
|
+
}
|
|
154
|
+
"
|
|
151
155
|
>
|
|
152
156
|
{{ t('spec.mode.structured') }}
|
|
153
157
|
</UButton>
|
|
@@ -158,7 +162,11 @@ function kindLabel(item: RequirementItem): string {
|
|
|
158
162
|
icon="i-lucide-square-check-big"
|
|
159
163
|
:disabled="!hasGherkin"
|
|
160
164
|
:title="hasGherkin ? t('spec.mode.gherkinTooltip') : t('spec.mode.gherkinNone')"
|
|
161
|
-
@click="
|
|
165
|
+
@click="
|
|
166
|
+
() => {
|
|
167
|
+
mode = 'gherkin'
|
|
168
|
+
}
|
|
169
|
+
"
|
|
162
170
|
>
|
|
163
171
|
{{ t('spec.mode.gherkin') }}
|
|
164
172
|
</UButton>
|
|
@@ -210,7 +218,11 @@ function kindLabel(item: RequirementItem): string {
|
|
|
210
218
|
:variant="selected === null ? 'soft' : 'ghost'"
|
|
211
219
|
size="xs"
|
|
212
220
|
icon="i-lucide-info"
|
|
213
|
-
@click="
|
|
221
|
+
@click="
|
|
222
|
+
() => {
|
|
223
|
+
selected = null
|
|
224
|
+
}
|
|
225
|
+
"
|
|
214
226
|
>
|
|
215
227
|
{{ t('spec.overview') }}
|
|
216
228
|
</UButton>
|
|
@@ -444,7 +444,11 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
|
|
|
444
444
|
size="xs"
|
|
445
445
|
class="mt-1.5"
|
|
446
446
|
data-testid="tester-infra-setup-logs-toggle"
|
|
447
|
-
@click="
|
|
447
|
+
@click="
|
|
448
|
+
() => {
|
|
449
|
+
showInfraSetupLogs = !showInfraSetupLogs
|
|
450
|
+
}
|
|
451
|
+
"
|
|
448
452
|
>
|
|
449
453
|
{{
|
|
450
454
|
showInfraSetupLogs
|
|
@@ -480,7 +484,11 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
|
|
|
480
484
|
variant="ghost"
|
|
481
485
|
size="xs"
|
|
482
486
|
data-testid="tester-infra-attempts-toggle"
|
|
483
|
-
@click="
|
|
487
|
+
@click="
|
|
488
|
+
() => {
|
|
489
|
+
showProvisioning = !showProvisioning
|
|
490
|
+
}
|
|
491
|
+
"
|
|
484
492
|
>
|
|
485
493
|
{{
|
|
486
494
|
showProvisioning
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
answerDocInterviewContract,
|
|
3
|
+
continueDocInterviewContract,
|
|
4
|
+
getDocInterviewContract,
|
|
5
|
+
proceedDocInterviewContract,
|
|
6
|
+
} from '@cat-factory/contracts'
|
|
7
|
+
import type { ApiContext } from './context'
|
|
8
|
+
|
|
9
|
+
/** Interactive document-interview session (WS5): load + answer / continue / proceed. */
|
|
10
|
+
export function docInterviewApi({ send, ws }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
// The interview window's load path: the session anchored to a board block (or null).
|
|
13
|
+
getDocInterview: (workspaceId: string, blockId: string) =>
|
|
14
|
+
send(getDocInterviewContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
|
|
15
|
+
|
|
16
|
+
// Answer one interview question (no run resume), then continue (interviewer re-runs, may
|
|
17
|
+
// ask more) or proceed (skip remaining, synthesize the brief and advance to the writer).
|
|
18
|
+
answerDocInterview: (
|
|
19
|
+
workspaceId: string,
|
|
20
|
+
blockId: string,
|
|
21
|
+
questionId: string,
|
|
22
|
+
answer: string,
|
|
23
|
+
) =>
|
|
24
|
+
send(answerDocInterviewContract, {
|
|
25
|
+
pathPrefix: ws(workspaceId),
|
|
26
|
+
pathParams: { blockId },
|
|
27
|
+
body: { questionId, answer },
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
continueDocInterview: (workspaceId: string, blockId: string) =>
|
|
31
|
+
send(continueDocInterviewContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
|
|
32
|
+
|
|
33
|
+
proceedDocInterview: (workspaceId: string, blockId: string) =>
|
|
34
|
+
send(proceedDocInterviewContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSharedStackContract,
|
|
3
|
+
deleteSharedStackContract,
|
|
4
|
+
ensureSharedStackUpContract,
|
|
5
|
+
listSharedStacksContract,
|
|
6
|
+
teardownSharedStackContract,
|
|
7
|
+
updateSharedStackContract,
|
|
8
|
+
} from '@cat-factory/contracts'
|
|
9
|
+
import type { UpdateSharedStackInput } from '~/types/sharedStacks'
|
|
10
|
+
import type { SendParams } from './client'
|
|
11
|
+
import type { ApiContext } from './context'
|
|
12
|
+
|
|
13
|
+
// The create body is typed from the contract's INPUT shape so the valibot-defaulted array fields
|
|
14
|
+
// (profiles, envFiles, managedNetworks, setupSteps, allowHostCommands) stay optional for callers.
|
|
15
|
+
type CreateSharedStackBody = NonNullable<SendParams<typeof createSharedStackContract>['body']>
|
|
16
|
+
|
|
17
|
+
/** A workspace's shared stacks: CRUD plus the ensure-up / teardown lifecycle actions. */
|
|
18
|
+
export function sharedStacksApi({ send, ws }: ApiContext) {
|
|
19
|
+
return {
|
|
20
|
+
listSharedStacks: (workspaceId: string) =>
|
|
21
|
+
send(listSharedStacksContract, { pathPrefix: ws(workspaceId) }),
|
|
22
|
+
|
|
23
|
+
createSharedStack: (workspaceId: string, body: CreateSharedStackBody) =>
|
|
24
|
+
send(createSharedStackContract, { pathPrefix: ws(workspaceId), body }),
|
|
25
|
+
|
|
26
|
+
updateSharedStack: (workspaceId: string, stackId: string, body: UpdateSharedStackInput) =>
|
|
27
|
+
send(updateSharedStackContract, {
|
|
28
|
+
pathPrefix: ws(workspaceId),
|
|
29
|
+
pathParams: { stackId },
|
|
30
|
+
body,
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
deleteSharedStack: (workspaceId: string, stackId: string) =>
|
|
34
|
+
send(deleteSharedStackContract, { pathPrefix: ws(workspaceId), pathParams: { stackId } }),
|
|
35
|
+
|
|
36
|
+
ensureSharedStackUp: (workspaceId: string, stackId: string) =>
|
|
37
|
+
send(ensureSharedStackUpContract, { pathPrefix: ws(workspaceId), pathParams: { stackId } }),
|
|
38
|
+
|
|
39
|
+
teardownSharedStack: (workspaceId: string, stackId: string) =>
|
|
40
|
+
send(teardownSharedStackContract, { pathPrefix: ws(workspaceId), pathParams: { stackId } }),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -14,6 +14,7 @@ import { humanReviewApi } from './api/humanReview'
|
|
|
14
14
|
import { humanTestApi } from './api/humanTest'
|
|
15
15
|
import { infraHandlersApi } from './api/infraHandlers'
|
|
16
16
|
import { initiativeApi } from './api/initiative'
|
|
17
|
+
import { docInterviewApi } from './api/docInterview'
|
|
17
18
|
import { visualConfirmApi } from './api/visualConfirm'
|
|
18
19
|
import { kaizenApi } from './api/kaizen'
|
|
19
20
|
import { localSettingsApi } from './api/localSettings'
|
|
@@ -21,6 +22,7 @@ import { modelsApi } from './api/models'
|
|
|
21
22
|
import { notificationsApi } from './api/notifications'
|
|
22
23
|
import { packageRegistriesApi } from './api/packageRegistries'
|
|
23
24
|
import { presetsApi } from './api/presets'
|
|
25
|
+
import { sharedStacksApi } from './api/sharedStacks'
|
|
24
26
|
import { providerConnectionsApi } from './api/providerConnections'
|
|
25
27
|
import { provisioningLogsApi } from './api/provisioningLogs'
|
|
26
28
|
import { recurringApi } from './api/recurring'
|
|
@@ -111,9 +113,11 @@ export function useApi() {
|
|
|
111
113
|
...specApi(ctx),
|
|
112
114
|
...notificationsApi(ctx),
|
|
113
115
|
...presetsApi(ctx),
|
|
116
|
+
...sharedStacksApi(ctx),
|
|
114
117
|
...providerConnectionsApi(ctx),
|
|
115
118
|
...infraHandlersApi(ctx),
|
|
116
119
|
...initiativeApi(ctx),
|
|
120
|
+
...docInterviewApi(ctx),
|
|
117
121
|
...provisioningLogsApi(ctx),
|
|
118
122
|
...releaseHealthApi(ctx),
|
|
119
123
|
...packageRegistriesApi(ctx),
|
|
@@ -26,6 +26,7 @@ export function useWorkspaceStream() {
|
|
|
26
26
|
const brainstorm = useBrainstormStore()
|
|
27
27
|
const kaizen = useKaizenStore()
|
|
28
28
|
const initiatives = useInitiativesStore()
|
|
29
|
+
const docInterview = useDocInterviewStore()
|
|
29
30
|
const api = useApi()
|
|
30
31
|
const apiBase = useRuntimeConfig().public.apiBase
|
|
31
32
|
|
|
@@ -108,6 +109,10 @@ export function useWorkspaceStream() {
|
|
|
108
109
|
// An initiative changed (created, plan ingested, an item settled) — patch the cache
|
|
109
110
|
// so an open tracker window / the board card reflects the transition live.
|
|
110
111
|
initiatives.upsert(event.initiative)
|
|
112
|
+
} else if (event.type === 'docInterview') {
|
|
113
|
+
// The interactive document interview advanced (a fresh batch of questions, an answer, or
|
|
114
|
+
// convergence) — patch the cache so an open interview window reflects it live.
|
|
115
|
+
docInterview.upsert(event.session)
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { DocInterviewSession } from '~/types/domain'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interactive document-interview sessions (WS5), keyed by their anchor BLOCK id. Loaded on
|
|
8
|
+
* demand when the interview window opens (`load`) and patched live from `docInterview` stream
|
|
9
|
+
* events (`upsert`), so an open window follows the interview as the interviewer asks / converges.
|
|
10
|
+
* Not carried in the workspace snapshot (a transient per-run gate, unlike initiatives).
|
|
11
|
+
* Per-workspace; nothing is persisted client-side.
|
|
12
|
+
*/
|
|
13
|
+
export const useDocInterviewStore = defineStore('docInterview', () => {
|
|
14
|
+
const api = useApi()
|
|
15
|
+
const workspace = useWorkspaceStore()
|
|
16
|
+
|
|
17
|
+
/** The sessions keyed by their anchor block id. */
|
|
18
|
+
const byBlock = ref<Record<string, DocInterviewSession>>({})
|
|
19
|
+
/** True while a window action (continue/proceed) is resuming the run. */
|
|
20
|
+
const resuming = ref(false)
|
|
21
|
+
|
|
22
|
+
function forBlock(blockId: string): DocInterviewSession | null {
|
|
23
|
+
return byBlock.value[blockId] ?? null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Patch from a live `docInterview` stream event or a call response (newest write wins). */
|
|
27
|
+
function upsert(session: DocInterviewSession) {
|
|
28
|
+
const existing = byBlock.value[session.blockId]
|
|
29
|
+
if (existing && existing.updatedAt > session.updatedAt) return
|
|
30
|
+
byBlock.value = { ...byBlock.value, [session.blockId]: session }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Re-fetch one block's session (the interview window's load path). */
|
|
34
|
+
async function load(blockId: string) {
|
|
35
|
+
if (!workspace.workspaceId) return
|
|
36
|
+
const session = await api.getDocInterview(workspace.workspaceId, blockId)
|
|
37
|
+
if (session) upsert(session)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Record the human's answer to one pending interview question (no run resume). */
|
|
41
|
+
async function answerQuestion(blockId: string, questionId: string, answer: string) {
|
|
42
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
43
|
+
const updated = await api.answerDocInterview(workspace.workspaceId, blockId, questionId, answer)
|
|
44
|
+
upsert(updated)
|
|
45
|
+
return updated
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Submit the answers and resume the interview (the interviewer re-runs, may ask more). */
|
|
49
|
+
async function continueInterview(blockId: string) {
|
|
50
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
51
|
+
resuming.value = true
|
|
52
|
+
try {
|
|
53
|
+
const updated = await api.continueDocInterview(workspace.workspaceId, blockId)
|
|
54
|
+
upsert(updated)
|
|
55
|
+
return updated
|
|
56
|
+
} finally {
|
|
57
|
+
resuming.value = false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Skip remaining questions: the interviewer converges and the run advances to the writer. */
|
|
62
|
+
async function proceedInterview(blockId: string) {
|
|
63
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
64
|
+
resuming.value = true
|
|
65
|
+
try {
|
|
66
|
+
const updated = await api.proceedDocInterview(workspace.workspaceId, blockId)
|
|
67
|
+
upsert(updated)
|
|
68
|
+
return updated
|
|
69
|
+
} finally {
|
|
70
|
+
resuming.value = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function reset() {
|
|
75
|
+
byBlock.value = {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
byBlock,
|
|
80
|
+
resuming,
|
|
81
|
+
forBlock,
|
|
82
|
+
upsert,
|
|
83
|
+
load,
|
|
84
|
+
answerQuestion,
|
|
85
|
+
continueInterview,
|
|
86
|
+
proceedInterview,
|
|
87
|
+
reset,
|
|
88
|
+
}
|
|
89
|
+
})
|