@cat-factory/app 0.69.1 → 0.70.1
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/AgentFailureCard.vue +3 -0
- package/app/components/board/TaskDependencyEdges.vue +66 -0
- package/app/components/board/nodes/BlockNode.vue +10 -2
- package/app/components/board/nodes/TaskCard.vue +12 -1
- package/app/components/common/ConfirmDialog.vue +59 -0
- package/app/components/common/EmptyState.vue +35 -0
- package/app/components/common/KeyboardShortcutsHelp.vue +48 -0
- package/app/components/documents/ContextDocumentPicker.vue +8 -4
- package/app/components/fragments/FragmentLibraryManager.vue +10 -0
- package/app/components/github/GitHubPanel.vue +9 -0
- package/app/components/layout/BoardSwitcher.vue +11 -0
- package/app/components/layout/CommandBar.vue +8 -0
- package/app/components/layout/NotificationsInbox.vue +11 -0
- package/app/components/panels/InspectorPanel.vue +9 -13
- package/app/components/panels/StepContainerStatus.vue +37 -0
- package/app/components/panels/inspector/FrontendConfig.vue +349 -0
- package/app/components/panels/inspector/TaskDependencies.vue +18 -4
- package/app/components/panels/inspector/TaskExecution.vue +24 -0
- package/app/components/pipeline/PipelineBuilder.vue +13 -1
- package/app/components/providers/PersonalSubscriptionSection.vue +9 -0
- package/app/components/providers/VendorCredentialsModal.vue +12 -3
- package/app/components/settings/LocalModelEndpointsPanel.vue +11 -0
- package/app/components/settings/MergeThresholdsPanel.vue +9 -0
- package/app/components/settings/ModelConfigurationPanel.vue +9 -0
- package/app/components/settings/UserSecretsSection.vue +11 -0
- package/app/components/slack/SlackPanel.vue +9 -0
- package/app/components/tasks/ContextIssuePicker.vue +8 -4
- package/app/composables/useBlockDeletion.ts +63 -0
- package/app/composables/useConfirm.ts +63 -0
- package/app/composables/useKeyboardShortcuts.ts +76 -0
- package/app/pages/index.vue +8 -0
- package/app/stores/agentRuns.spec.ts +56 -0
- package/app/stores/agentRuns.ts +22 -4
- package/app/stores/ui.ts +16 -0
- package/app/types/domain.ts +7 -0
- package/i18n/locales/en.json +131 -5
- package/i18n/locales/es.json +135 -9
- package/i18n/locales/fr.json +135 -9
- package/i18n/locales/he.json +135 -9
- package/i18n/locales/ja.json +135 -9
- package/i18n/locales/pl.json +135 -9
- package/i18n/locales/tr.json +135 -9
- package/i18n/locales/uk.json +135 -9
- package/package.json +2 -2
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
Block,
|
|
5
|
+
FrontendBackendBinding,
|
|
6
|
+
FrontendConfig,
|
|
7
|
+
FrontendEnvInjection,
|
|
8
|
+
FrontendPackageManager,
|
|
9
|
+
FrontendServeMode,
|
|
10
|
+
} from '~/types/domain'
|
|
11
|
+
|
|
12
|
+
// Frontend-frame (`type: 'frontend'`) configuration: how to build, serve, and mock this
|
|
13
|
+
// frontend for a self-contained UI test (+ an optional browsable preview on local/node),
|
|
14
|
+
// and its backend bindings. Each binding names an env var the frontend reads for an upstream
|
|
15
|
+
// URL and where that URL resolves — a bound SERVICE frame's ephemeral env (the service under
|
|
16
|
+
// test), or WireMock. The bindings ARE the board's frontend→service links. Persisted as a
|
|
17
|
+
// serialized FrontendConfig on the block via the shared updateBlock PATCH.
|
|
18
|
+
const props = defineProps<{ block: Block }>()
|
|
19
|
+
|
|
20
|
+
const board = useBoardStore()
|
|
21
|
+
const { t } = useI18n()
|
|
22
|
+
|
|
23
|
+
const config = computed<FrontendConfig>(() => props.block.frontendConfig ?? { backendBindings: [] })
|
|
24
|
+
const bindings = computed(() => config.value.backendBindings ?? [])
|
|
25
|
+
|
|
26
|
+
// Merge a partial onto the current config, preserving the other fields, and persist. A field
|
|
27
|
+
// set to undefined is dropped (JSON.stringify omits it), so the harness default applies.
|
|
28
|
+
function save(patch: Partial<FrontendConfig>) {
|
|
29
|
+
const base: FrontendConfig = props.block.frontendConfig ?? { backendBindings: [] }
|
|
30
|
+
board.updateBlock(props.block.id, { frontendConfig: { ...base, ...patch } })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A trimmed string field: an empty value clears it (undefined) so the harness default applies.
|
|
34
|
+
function saveText(field: keyof FrontendConfig, value: string) {
|
|
35
|
+
save({ [field]: value.trim() || undefined } as Partial<FrontendConfig>)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The serve port must be an integer in [1, 65535] (the contract's schema bounds). Coerce and
|
|
39
|
+
// clamp to a valid port, dropping anything else to undefined (clears it → the harness default),
|
|
40
|
+
// so an out-of-range or non-integer value never 422s the PATCH.
|
|
41
|
+
function saveServePort(value: string) {
|
|
42
|
+
const n = Math.trunc(Number(value.trim()))
|
|
43
|
+
save({ servePort: Number.isInteger(n) && n >= 1 && n <= 65535 ? n : undefined })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PACKAGE_MANAGERS: FrontendPackageManager[] = ['pnpm', 'npm', 'yarn']
|
|
47
|
+
const packageManager = computed(() => config.value.packageManager ?? 'pnpm')
|
|
48
|
+
const serveMode = computed<FrontendServeMode>(() => config.value.serveMode ?? 'static')
|
|
49
|
+
const envInjection = computed<FrontendEnvInjection>(() => config.value.envInjection ?? 'build')
|
|
50
|
+
|
|
51
|
+
// The service frames on the board a binding can point at (its ephemeral env URL becomes the
|
|
52
|
+
// service under test). Every other binding resolves to WireMock.
|
|
53
|
+
const serviceFrames = computed(() => board.frames.filter((b) => b.type === 'service'))
|
|
54
|
+
|
|
55
|
+
// USelect options for a binding's source: WireMock, or one of the service frames.
|
|
56
|
+
const sourceItems = computed(() => [
|
|
57
|
+
{ label: t('inspector.frontendConfig.bindings.mock'), value: 'mock' },
|
|
58
|
+
...serviceFrames.value.map((f) => ({ label: f.title || f.id, value: f.id })),
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
// The select value for a binding: 'mock', or the bound service block id.
|
|
62
|
+
function sourceValue(binding: FrontendBackendBinding): string {
|
|
63
|
+
return binding.source.kind === 'service' ? binding.source.serviceBlockId : 'mock'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function replaceBinding(index: number, next: FrontendBackendBinding) {
|
|
67
|
+
save({ backendBindings: bindings.value.map((b, i) => (i === index ? next : b)) })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setBindingEnvVar(index: number, value: string) {
|
|
71
|
+
const b = bindings.value[index]
|
|
72
|
+
if (b) replaceBinding(index, { ...b, envVar: value.trim() })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setBindingSource(index: number, value: string) {
|
|
76
|
+
const b = bindings.value[index]
|
|
77
|
+
if (!b) return
|
|
78
|
+
const source: FrontendBackendBinding['source'] =
|
|
79
|
+
value === 'mock' ? { kind: 'mock' } : { kind: 'service', serviceBlockId: value }
|
|
80
|
+
replaceBinding(index, { ...b, source })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addBinding() {
|
|
84
|
+
save({ backendBindings: [...bindings.value, { envVar: '', source: { kind: 'mock' } }] })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function removeBinding(index: number) {
|
|
88
|
+
save({ backendBindings: bindings.value.filter((_, i) => i !== index) })
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<div class="space-y-3">
|
|
94
|
+
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
95
|
+
{{ t('inspector.frontendConfig.title') }}
|
|
96
|
+
</div>
|
|
97
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
98
|
+
{{ t('inspector.frontendConfig.hint') }}
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
<!-- Package manager -->
|
|
102
|
+
<div class="space-y-1">
|
|
103
|
+
<span class="text-[11px] text-slate-400">{{
|
|
104
|
+
t('inspector.frontendConfig.packageManager')
|
|
105
|
+
}}</span>
|
|
106
|
+
<div class="flex flex-wrap gap-1">
|
|
107
|
+
<UButton
|
|
108
|
+
v-for="pm in PACKAGE_MANAGERS"
|
|
109
|
+
:key="pm"
|
|
110
|
+
:color="packageManager === pm ? 'primary' : 'neutral'"
|
|
111
|
+
:variant="packageManager === pm ? 'soft' : 'ghost'"
|
|
112
|
+
size="xs"
|
|
113
|
+
@click="save({ packageManager: pm })"
|
|
114
|
+
>
|
|
115
|
+
{{ pm }}
|
|
116
|
+
</UButton>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- Build: install command + build script + output dir -->
|
|
121
|
+
<div class="space-y-1">
|
|
122
|
+
<label class="text-[11px] text-slate-400">{{
|
|
123
|
+
t('inspector.frontendConfig.installCommand')
|
|
124
|
+
}}</label>
|
|
125
|
+
<UInput
|
|
126
|
+
:model-value="config.installCommand ?? ''"
|
|
127
|
+
size="xs"
|
|
128
|
+
class="font-mono"
|
|
129
|
+
maxlength="400"
|
|
130
|
+
placeholder="pnpm install --frozen-lockfile"
|
|
131
|
+
@blur="(e: FocusEvent) => saveText('installCommand', (e.target as HTMLInputElement).value)"
|
|
132
|
+
@keydown.enter="
|
|
133
|
+
(e: KeyboardEvent) => saveText('installCommand', (e.target as HTMLInputElement).value)
|
|
134
|
+
"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="grid grid-cols-2 gap-2">
|
|
139
|
+
<div class="space-y-1">
|
|
140
|
+
<label class="text-[11px] text-slate-400">{{
|
|
141
|
+
t('inspector.frontendConfig.buildScript')
|
|
142
|
+
}}</label>
|
|
143
|
+
<UInput
|
|
144
|
+
:model-value="config.buildScript ?? ''"
|
|
145
|
+
size="xs"
|
|
146
|
+
class="font-mono"
|
|
147
|
+
maxlength="200"
|
|
148
|
+
placeholder="build"
|
|
149
|
+
@blur="(e: FocusEvent) => saveText('buildScript', (e.target as HTMLInputElement).value)"
|
|
150
|
+
@keydown.enter="
|
|
151
|
+
(e: KeyboardEvent) => saveText('buildScript', (e.target as HTMLInputElement).value)
|
|
152
|
+
"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="space-y-1">
|
|
156
|
+
<label class="text-[11px] text-slate-400">{{
|
|
157
|
+
t('inspector.frontendConfig.outputDir')
|
|
158
|
+
}}</label>
|
|
159
|
+
<UInput
|
|
160
|
+
:model-value="config.outputDir ?? ''"
|
|
161
|
+
size="xs"
|
|
162
|
+
class="font-mono"
|
|
163
|
+
maxlength="400"
|
|
164
|
+
placeholder="dist"
|
|
165
|
+
@blur="(e: FocusEvent) => saveText('outputDir', (e.target as HTMLInputElement).value)"
|
|
166
|
+
@keydown.enter="
|
|
167
|
+
(e: KeyboardEvent) => saveText('outputDir', (e.target as HTMLInputElement).value)
|
|
168
|
+
"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<!-- Serve: mode (static vs command) + serve script (command mode) + port -->
|
|
174
|
+
<div class="space-y-1">
|
|
175
|
+
<span class="text-[11px] text-slate-400">{{ t('inspector.frontendConfig.serveMode') }}</span>
|
|
176
|
+
<div class="flex flex-wrap gap-1">
|
|
177
|
+
<UButton
|
|
178
|
+
:color="serveMode === 'static' ? 'primary' : 'neutral'"
|
|
179
|
+
:variant="serveMode === 'static' ? 'soft' : 'ghost'"
|
|
180
|
+
size="xs"
|
|
181
|
+
@click="save({ serveMode: 'static' })"
|
|
182
|
+
>
|
|
183
|
+
{{ t('inspector.frontendConfig.serveStatic') }}
|
|
184
|
+
</UButton>
|
|
185
|
+
<UButton
|
|
186
|
+
:color="serveMode === 'command' ? 'primary' : 'neutral'"
|
|
187
|
+
:variant="serveMode === 'command' ? 'soft' : 'ghost'"
|
|
188
|
+
size="xs"
|
|
189
|
+
@click="save({ serveMode: 'command' })"
|
|
190
|
+
>
|
|
191
|
+
{{ t('inspector.frontendConfig.serveCommand') }}
|
|
192
|
+
</UButton>
|
|
193
|
+
</div>
|
|
194
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
195
|
+
{{ t('inspector.frontendConfig.serveModeHint') }}
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div v-if="serveMode === 'command'" class="space-y-1">
|
|
200
|
+
<label class="text-[11px] text-slate-400">{{
|
|
201
|
+
t('inspector.frontendConfig.serveScript')
|
|
202
|
+
}}</label>
|
|
203
|
+
<UInput
|
|
204
|
+
:model-value="config.serveScript ?? ''"
|
|
205
|
+
size="xs"
|
|
206
|
+
class="font-mono"
|
|
207
|
+
maxlength="200"
|
|
208
|
+
placeholder="preview"
|
|
209
|
+
@blur="(e: FocusEvent) => saveText('serveScript', (e.target as HTMLInputElement).value)"
|
|
210
|
+
@keydown.enter="
|
|
211
|
+
(e: KeyboardEvent) => saveText('serveScript', (e.target as HTMLInputElement).value)
|
|
212
|
+
"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div class="grid grid-cols-2 gap-2">
|
|
217
|
+
<div class="space-y-1">
|
|
218
|
+
<label class="text-[11px] text-slate-400">{{
|
|
219
|
+
t('inspector.frontendConfig.servePort')
|
|
220
|
+
}}</label>
|
|
221
|
+
<UInput
|
|
222
|
+
:model-value="config.servePort != null ? String(config.servePort) : ''"
|
|
223
|
+
type="number"
|
|
224
|
+
min="1"
|
|
225
|
+
max="65535"
|
|
226
|
+
step="1"
|
|
227
|
+
size="xs"
|
|
228
|
+
class="font-mono"
|
|
229
|
+
placeholder="8080"
|
|
230
|
+
@blur="(e: FocusEvent) => saveServePort((e.target as HTMLInputElement).value)"
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="space-y-1">
|
|
234
|
+
<label class="text-[11px] text-slate-400">{{
|
|
235
|
+
t('inspector.frontendConfig.mockMappingsPath')
|
|
236
|
+
}}</label>
|
|
237
|
+
<UInput
|
|
238
|
+
:model-value="config.mockMappingsPath ?? ''"
|
|
239
|
+
size="xs"
|
|
240
|
+
class="font-mono"
|
|
241
|
+
maxlength="400"
|
|
242
|
+
placeholder="mocks/"
|
|
243
|
+
@blur="
|
|
244
|
+
(e: FocusEvent) => saveText('mockMappingsPath', (e.target as HTMLInputElement).value)
|
|
245
|
+
"
|
|
246
|
+
@keydown.enter="
|
|
247
|
+
(e: KeyboardEvent) => saveText('mockMappingsPath', (e.target as HTMLInputElement).value)
|
|
248
|
+
"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<!-- Env injection: build-time env vars vs a runtime window.env shim -->
|
|
254
|
+
<div class="space-y-1">
|
|
255
|
+
<span class="text-[11px] text-slate-400">{{
|
|
256
|
+
t('inspector.frontendConfig.envInjection')
|
|
257
|
+
}}</span>
|
|
258
|
+
<div class="flex flex-wrap gap-1">
|
|
259
|
+
<UButton
|
|
260
|
+
:color="envInjection === 'build' ? 'primary' : 'neutral'"
|
|
261
|
+
:variant="envInjection === 'build' ? 'soft' : 'ghost'"
|
|
262
|
+
size="xs"
|
|
263
|
+
@click="save({ envInjection: 'build' })"
|
|
264
|
+
>
|
|
265
|
+
{{ t('inspector.frontendConfig.envBuild') }}
|
|
266
|
+
</UButton>
|
|
267
|
+
<UButton
|
|
268
|
+
:color="envInjection === 'runtime' ? 'primary' : 'neutral'"
|
|
269
|
+
:variant="envInjection === 'runtime' ? 'soft' : 'ghost'"
|
|
270
|
+
size="xs"
|
|
271
|
+
@click="save({ envInjection: 'runtime' })"
|
|
272
|
+
>
|
|
273
|
+
{{ t('inspector.frontendConfig.envRuntime') }}
|
|
274
|
+
</UButton>
|
|
275
|
+
</div>
|
|
276
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
277
|
+
{{ t('inspector.frontendConfig.envInjectionHint') }}
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<!-- Backend bindings: env var → upstream. These double as the board's frontend→service links. -->
|
|
282
|
+
<div class="space-y-2 border-t border-slate-800 pt-2">
|
|
283
|
+
<div class="flex items-center justify-between">
|
|
284
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
285
|
+
{{ t('inspector.frontendConfig.bindings.title') }}
|
|
286
|
+
</span>
|
|
287
|
+
<UButton
|
|
288
|
+
size="xs"
|
|
289
|
+
variant="ghost"
|
|
290
|
+
color="neutral"
|
|
291
|
+
icon="i-lucide-plus"
|
|
292
|
+
@click="addBinding"
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
296
|
+
{{ t('inspector.frontendConfig.bindings.hint') }}
|
|
297
|
+
</p>
|
|
298
|
+
|
|
299
|
+
<div v-if="bindings.length" class="space-y-1.5">
|
|
300
|
+
<div v-for="(b, i) in bindings" :key="i" class="flex items-center gap-1">
|
|
301
|
+
<UInput
|
|
302
|
+
:model-value="b.envVar"
|
|
303
|
+
size="xs"
|
|
304
|
+
class="flex-1 font-mono"
|
|
305
|
+
maxlength="200"
|
|
306
|
+
placeholder="PUB_BACKEND_URL"
|
|
307
|
+
@blur="(e: FocusEvent) => setBindingEnvVar(i, (e.target as HTMLInputElement).value)"
|
|
308
|
+
@keydown.enter="
|
|
309
|
+
(e: KeyboardEvent) => setBindingEnvVar(i, (e.target as HTMLInputElement).value)
|
|
310
|
+
"
|
|
311
|
+
/>
|
|
312
|
+
<USelect
|
|
313
|
+
:model-value="sourceValue(b)"
|
|
314
|
+
:items="sourceItems"
|
|
315
|
+
size="xs"
|
|
316
|
+
class="flex-1"
|
|
317
|
+
@update:model-value="(v: string) => setBindingSource(i, v)"
|
|
318
|
+
/>
|
|
319
|
+
<UButton
|
|
320
|
+
size="xs"
|
|
321
|
+
variant="ghost"
|
|
322
|
+
color="neutral"
|
|
323
|
+
icon="i-lucide-x"
|
|
324
|
+
:title="t('inspector.frontendConfig.bindings.remove')"
|
|
325
|
+
@click="removeBinding(i)"
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
330
|
+
{{ t('inspector.frontendConfig.bindings.empty') }}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<!-- Browsable preview (local/node only). -->
|
|
335
|
+
<div class="border-t border-slate-800 pt-2">
|
|
336
|
+
<UCheckbox
|
|
337
|
+
:model-value="config.previewEnabled === true"
|
|
338
|
+
:label="t('inspector.frontendConfig.previewEnabled')"
|
|
339
|
+
size="xs"
|
|
340
|
+
@update:model-value="
|
|
341
|
+
(v: boolean | 'indeterminate') => save({ previewEnabled: v === true ? true : undefined })
|
|
342
|
+
"
|
|
343
|
+
/>
|
|
344
|
+
<p class="mt-1 text-[11px] leading-snug text-slate-500">
|
|
345
|
+
{{ t('inspector.frontendConfig.previewHint') }}
|
|
346
|
+
</p>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</template>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Block } from '~/types/domain'
|
|
3
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
3
4
|
|
|
4
5
|
const props = defineProps<{ block: Block }>()
|
|
5
6
|
|
|
6
7
|
const board = useBoardStore()
|
|
7
8
|
const { depLabel } = useDepLabels()
|
|
9
|
+
const { confirm } = useConfirm()
|
|
8
10
|
const { t } = useI18n()
|
|
9
11
|
|
|
10
12
|
const deps = computed(() =>
|
|
@@ -27,8 +29,15 @@ const depMenu = computed(() => {
|
|
|
27
29
|
}))
|
|
28
30
|
})
|
|
29
31
|
|
|
30
|
-
function removeDep(
|
|
31
|
-
|
|
32
|
+
async function removeDep(dep: Block) {
|
|
33
|
+
const ok = await confirm({
|
|
34
|
+
title: t('inspector.dependencies.confirmRemove.title'),
|
|
35
|
+
description: t('inspector.dependencies.confirmRemove.body', { name: label(dep) }),
|
|
36
|
+
variant: 'destructive',
|
|
37
|
+
confirmLabel: t('common.remove'),
|
|
38
|
+
icon: 'i-lucide-unlink',
|
|
39
|
+
})
|
|
40
|
+
if (ok) board.removeDependency(props.block.id, dep.id)
|
|
32
41
|
}
|
|
33
42
|
</script>
|
|
34
43
|
|
|
@@ -61,13 +70,18 @@ function removeDep(depId: string) {
|
|
|
61
70
|
? t('inspector.dependencies.merged')
|
|
62
71
|
: t('inspector.dependencies.notMerged')
|
|
63
72
|
"
|
|
64
|
-
@click="removeDep(d
|
|
73
|
+
@click="removeDep(d)"
|
|
65
74
|
>
|
|
66
75
|
{{ label(d) }}
|
|
67
76
|
<UIcon name="i-lucide-x" class="ms-0.5 h-3 w-3" />
|
|
68
77
|
</UBadge>
|
|
69
78
|
</div>
|
|
70
|
-
<
|
|
79
|
+
<EmptyState
|
|
80
|
+
v-else
|
|
81
|
+
compact
|
|
82
|
+
icon="i-lucide-git-branch"
|
|
83
|
+
:title="t('inspector.dependencies.empty')"
|
|
84
|
+
/>
|
|
71
85
|
<div v-if="!runnable" class="mt-1 text-[10px] text-amber-400">
|
|
72
86
|
{{ t('inspector.dependencies.blocked') }}
|
|
73
87
|
</div>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
containerPhaseLabel,
|
|
9
9
|
} from '~/utils/pipelineRender'
|
|
10
10
|
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
11
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
11
12
|
|
|
12
13
|
const props = defineProps<{ block: Block }>()
|
|
13
14
|
|
|
@@ -34,6 +35,16 @@ const reviewStageLabel = computed(() =>
|
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
const instance = computed(() => execution.getInstance(props.block.executionId))
|
|
38
|
+
|
|
39
|
+
// Nothing to show yet: no run, no failed run, no PR, and not awaiting a merge — render an
|
|
40
|
+
// empty state instead of a blank gap so the section reads as "no runs yet" rather than broken.
|
|
41
|
+
const isEmpty = computed(
|
|
42
|
+
() =>
|
|
43
|
+
!instance.value &&
|
|
44
|
+
!failedRun.value &&
|
|
45
|
+
!props.block.pullRequest &&
|
|
46
|
+
props.block.status !== 'pr_ready',
|
|
47
|
+
)
|
|
37
48
|
// A failed run is no longer executing: a step left mid-flight must stop showing
|
|
38
49
|
// its live "Spinning up…" phase (the shared failure banner renders below).
|
|
39
50
|
const runFailed = computed(() => instance.value?.status === 'failed')
|
|
@@ -184,6 +195,9 @@ async function resetRun() {
|
|
|
184
195
|
:key="i"
|
|
185
196
|
class="rounded-md px-2 py-1"
|
|
186
197
|
:class="i === instance.currentStep ? 'bg-slate-800/70' : ''"
|
|
198
|
+
data-testid="run-step"
|
|
199
|
+
:data-step-kind="s.agentKind"
|
|
200
|
+
:data-step-state="s.state"
|
|
187
201
|
>
|
|
188
202
|
<div class="flex items-center gap-2">
|
|
189
203
|
<!-- Every agent is clickable: it opens the step-detail overlay (timing,
|
|
@@ -221,6 +235,7 @@ async function resetRun() {
|
|
|
221
235
|
<span
|
|
222
236
|
v-if="s.subtasks && s.subtasks.total > 0"
|
|
223
237
|
class="ms-auto font-mono text-[10px] tabular-nums text-slate-300"
|
|
238
|
+
data-testid="run-subtasks"
|
|
224
239
|
:title="
|
|
225
240
|
s.subtasks.inProgress > 0
|
|
226
241
|
? t('inspector.execution.subtasksProgress', {
|
|
@@ -399,6 +414,15 @@ async function resetRun() {
|
|
|
399
414
|
</p>
|
|
400
415
|
</div>
|
|
401
416
|
|
|
417
|
+
<!-- No run yet: read as "nothing here" rather than a blank gap. -->
|
|
418
|
+
<EmptyState
|
|
419
|
+
v-if="isEmpty"
|
|
420
|
+
compact
|
|
421
|
+
icon="i-lucide-play-circle"
|
|
422
|
+
:title="t('inspector.execution.empty.title')"
|
|
423
|
+
:description="t('inspector.execution.empty.body')"
|
|
424
|
+
/>
|
|
425
|
+
|
|
402
426
|
<!-- PR ready: merge -->
|
|
403
427
|
<UButton
|
|
404
428
|
v-if="block.status === 'pr_ready'"
|
|
@@ -198,6 +198,18 @@ function edit(p: Pipeline) {
|
|
|
198
198
|
pipelines.loadForEdit(p)
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
const { confirm } = useConfirm()
|
|
202
|
+
async function removePipeline(p: Pipeline) {
|
|
203
|
+
const ok = await confirm({
|
|
204
|
+
title: t('pipeline.builder.confirmDeletePipeline.title'),
|
|
205
|
+
description: t('pipeline.builder.confirmDeletePipeline.body', { name: p.name }),
|
|
206
|
+
variant: 'destructive',
|
|
207
|
+
confirmLabel: t('common.delete'),
|
|
208
|
+
icon: 'i-lucide-trash-2',
|
|
209
|
+
})
|
|
210
|
+
if (ok) void pipelines.removePipeline(p.id)
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
/** Clone any pipeline (incl. a read-only built-in) into an editable copy, then edit it. */
|
|
202
214
|
async function clone(p: Pipeline) {
|
|
203
215
|
try {
|
|
@@ -776,7 +788,7 @@ async function clone(p: Pipeline) {
|
|
|
776
788
|
variant="ghost"
|
|
777
789
|
size="xs"
|
|
778
790
|
:title="t('pipeline.builder.delete')"
|
|
779
|
-
@click="
|
|
791
|
+
@click="removePipeline(p)"
|
|
780
792
|
/>
|
|
781
793
|
</div>
|
|
782
794
|
</div>
|
|
@@ -13,6 +13,7 @@ const workspace = useWorkspaceStore()
|
|
|
13
13
|
const models = useModelsStore()
|
|
14
14
|
const toast = useToast()
|
|
15
15
|
const { t, d } = useI18n()
|
|
16
|
+
const { confirm } = useConfirm()
|
|
16
17
|
|
|
17
18
|
// Personal subscriptions are stored per-user, so they need a signed-in user. When there
|
|
18
19
|
// isn't one (a deployment running without sign-in), block the form so the user doesn't
|
|
@@ -141,6 +142,14 @@ async function connect() {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
async function disconnect(v: SubscriptionVendor) {
|
|
145
|
+
const ok = await confirm({
|
|
146
|
+
title: t('personalSubscriptions.confirmDisconnect.title'),
|
|
147
|
+
description: t('personalSubscriptions.confirmDisconnect.body', { vendor: vendorLabel(v) }),
|
|
148
|
+
variant: 'destructive',
|
|
149
|
+
confirmLabel: t('common.disconnect'),
|
|
150
|
+
icon: 'i-lucide-unplug',
|
|
151
|
+
})
|
|
152
|
+
if (!ok) return
|
|
144
153
|
try {
|
|
145
154
|
await personal.remove(v)
|
|
146
155
|
// Removing the subscription may drop the workspace's last usable model — refresh so the
|
|
@@ -14,6 +14,7 @@ const ui = useUiStore()
|
|
|
14
14
|
const workspace = useWorkspaceStore()
|
|
15
15
|
const creds = useVendorCredentialsStore()
|
|
16
16
|
const toast = useToast()
|
|
17
|
+
const { confirm } = useConfirm()
|
|
17
18
|
|
|
18
19
|
const open = computed({
|
|
19
20
|
get: () => ui.vendorCredentialsOpen,
|
|
@@ -139,9 +140,17 @@ async function add() {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
async function remove(id: string) {
|
|
143
|
+
async function remove(cred: { id: string; label: string }) {
|
|
144
|
+
const ok = await confirm({
|
|
145
|
+
title: t('providers.vendorCredentials.confirmRemove.title'),
|
|
146
|
+
description: t('providers.vendorCredentials.confirmRemove.body', { name: cred.label }),
|
|
147
|
+
variant: 'destructive',
|
|
148
|
+
confirmLabel: t('common.remove'),
|
|
149
|
+
icon: 'i-lucide-trash-2',
|
|
150
|
+
})
|
|
151
|
+
if (!ok) return
|
|
143
152
|
try {
|
|
144
|
-
await creds.remove(id)
|
|
153
|
+
await creds.remove(cred.id)
|
|
145
154
|
} catch (e) {
|
|
146
155
|
toast.add({
|
|
147
156
|
title: t('providers.vendorCredentials.toast.removeFailed'),
|
|
@@ -258,7 +267,7 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
258
267
|
color="error"
|
|
259
268
|
variant="ghost"
|
|
260
269
|
size="xs"
|
|
261
|
-
@click="remove(c
|
|
270
|
+
@click="remove(c)"
|
|
262
271
|
/>
|
|
263
272
|
</div>
|
|
264
273
|
</div>
|
|
@@ -13,6 +13,7 @@ const { t } = useI18n()
|
|
|
13
13
|
const ui = useUiStore()
|
|
14
14
|
const store = useLocalModelsStore()
|
|
15
15
|
const toast = useToast()
|
|
16
|
+
const { confirm } = useConfirm()
|
|
16
17
|
|
|
17
18
|
const open = computed({
|
|
18
19
|
get: () => ui.localModelsOpen,
|
|
@@ -140,6 +141,16 @@ async function save() {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
async function remove(p: LocalRunner) {
|
|
144
|
+
const ok = await confirm({
|
|
145
|
+
title: t('settings.localModelEndpoints.confirmRemove.title'),
|
|
146
|
+
description: t('settings.localModelEndpoints.confirmRemove.body', {
|
|
147
|
+
name: LOCAL_RUNNER_LABELS[p],
|
|
148
|
+
}),
|
|
149
|
+
variant: 'destructive',
|
|
150
|
+
confirmLabel: t('common.remove'),
|
|
151
|
+
icon: 'i-lucide-trash-2',
|
|
152
|
+
})
|
|
153
|
+
if (!ok) return
|
|
143
154
|
busy.value = true
|
|
144
155
|
try {
|
|
145
156
|
await store.remove(p)
|
|
@@ -29,6 +29,7 @@ const CONCERN_LEVELS = computed<{ value: RequirementConcernLevel; label: string
|
|
|
29
29
|
|
|
30
30
|
const store = useMergePresetsStore()
|
|
31
31
|
const toast = useToast()
|
|
32
|
+
const { confirm } = useConfirm()
|
|
32
33
|
|
|
33
34
|
// Local editable copy per preset, kept in sync with the store. Percentages are
|
|
34
35
|
// edited 0..100 and stored 0..1.
|
|
@@ -116,6 +117,14 @@ async function makeDefault(p: MergeThresholdPreset) {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
async function remove(p: MergeThresholdPreset) {
|
|
120
|
+
const ok = await confirm({
|
|
121
|
+
title: t('settings.mergeThresholds.confirmDelete.title'),
|
|
122
|
+
description: t('settings.mergeThresholds.confirmDelete.body', { name: p.name }),
|
|
123
|
+
variant: 'destructive',
|
|
124
|
+
confirmLabel: t('common.delete'),
|
|
125
|
+
icon: 'i-lucide-trash-2',
|
|
126
|
+
})
|
|
127
|
+
if (!ok) return
|
|
119
128
|
busy.value = p.id
|
|
120
129
|
try {
|
|
121
130
|
await store.remove(p.id)
|
|
@@ -25,6 +25,7 @@ const agents = useAgentsStore()
|
|
|
25
25
|
const creds = useVendorCredentialsStore()
|
|
26
26
|
const workspace = useWorkspaceStore()
|
|
27
27
|
const toast = useToast()
|
|
28
|
+
const { confirm } = useConfirm()
|
|
28
29
|
|
|
29
30
|
const open = computed({
|
|
30
31
|
get: () => ui.modelConfigOpen,
|
|
@@ -144,6 +145,14 @@ async function setDefault(p: ModelPreset) {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
async function remove(p: ModelPreset) {
|
|
148
|
+
const ok = await confirm({
|
|
149
|
+
title: t('settings.modelConfiguration.confirmDelete.title'),
|
|
150
|
+
description: t('settings.modelConfiguration.confirmDelete.body', { name: p.name }),
|
|
151
|
+
variant: 'destructive',
|
|
152
|
+
confirmLabel: t('common.delete'),
|
|
153
|
+
icon: 'i-lucide-trash-2',
|
|
154
|
+
})
|
|
155
|
+
if (!ok) return
|
|
147
156
|
busy.value = true
|
|
148
157
|
try {
|
|
149
158
|
await presets.remove(p.id)
|
|
@@ -12,6 +12,7 @@ const { t } = useI18n()
|
|
|
12
12
|
const ui = useUiStore()
|
|
13
13
|
const store = useUserSecretsStore()
|
|
14
14
|
const toast = useToast()
|
|
15
|
+
const { confirm } = useConfirm()
|
|
15
16
|
|
|
16
17
|
const open = computed({
|
|
17
18
|
get: () => ui.userSecretsOpen,
|
|
@@ -117,6 +118,16 @@ async function save() {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
async function remove() {
|
|
121
|
+
const ok = await confirm({
|
|
122
|
+
title: t('settings.userSecrets.confirmRemove.title'),
|
|
123
|
+
description: t('settings.userSecrets.confirmRemove.body', {
|
|
124
|
+
name: descriptor.value?.label ?? t('settings.userSecrets.secretFallback'),
|
|
125
|
+
}),
|
|
126
|
+
variant: 'destructive',
|
|
127
|
+
confirmLabel: t('common.remove'),
|
|
128
|
+
icon: 'i-lucide-trash-2',
|
|
129
|
+
})
|
|
130
|
+
if (!ok) return
|
|
120
131
|
busy.value = true
|
|
121
132
|
try {
|
|
122
133
|
await store.remove(kind.value)
|
|
@@ -13,6 +13,7 @@ const ui = useUiStore()
|
|
|
13
13
|
const slack = useSlackStore()
|
|
14
14
|
const toast = useToast()
|
|
15
15
|
const { t } = useI18n()
|
|
16
|
+
const { confirm } = useConfirm()
|
|
16
17
|
|
|
17
18
|
const open = computed({
|
|
18
19
|
get: () => ui.slackOpen,
|
|
@@ -108,6 +109,14 @@ async function connectWithToken() {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
async function disconnect() {
|
|
112
|
+
const ok = await confirm({
|
|
113
|
+
title: t('slack.confirmDisconnect.title'),
|
|
114
|
+
description: t('slack.confirmDisconnect.body'),
|
|
115
|
+
variant: 'destructive',
|
|
116
|
+
confirmLabel: t('common.disconnect'),
|
|
117
|
+
icon: 'i-lucide-unplug',
|
|
118
|
+
})
|
|
119
|
+
if (!ok) return
|
|
111
120
|
try {
|
|
112
121
|
await slack.disconnect()
|
|
113
122
|
} catch (e) {
|