@cat-factory/app 0.154.1 → 0.156.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/README.md +4 -3
- package/app/components/auth/LoginScreen.vue +11 -15
- package/app/components/github/GitHubOnboarding.vue +39 -11
- package/app/components/github/GitHubPanel.vue +58 -19
- package/app/components/panels/ResultWindowShell.vue +74 -3
- package/app/components/panels/StepValidationReport.vue +66 -0
- package/app/components/panels/inspector/ServiceValidationConfig.vue +208 -0
- package/app/components/vcs/GitLabConnect.vue +85 -0
- package/app/composables/api/validationChecks.ts +39 -0
- package/app/composables/api/vcs.ts +33 -0
- package/app/composables/useApi.ts +4 -0
- package/app/modular/panels/inspector.logic.spec.ts +3 -0
- package/app/modular/panels/inspector.logic.ts +5 -0
- package/app/modular/panels/inspector.ts +2 -0
- package/app/stores/github/connection.ts +13 -2
- package/app/stores/github/context.ts +3 -0
- package/app/stores/github/vcsConnect.ts +40 -0
- package/app/stores/github.spec.ts +146 -0
- package/app/stores/github.ts +49 -3
- package/app/stores/validationChecks.ts +111 -0
- package/app/types/domain.ts +1 -0
- package/app/types/validationChecks.ts +17 -0
- package/app/types/vcs.ts +14 -0
- package/app/utils/vcs.ts +31 -0
- package/i18n/locales/de.json +65 -7
- package/i18n/locales/en.json +65 -7
- package/i18n/locales/es.json +65 -7
- package/i18n/locales/fr.json +65 -7
- package/i18n/locales/he.json +65 -7
- package/i18n/locales/it.json +65 -7
- package/i18n/locales/ja.json +65 -7
- package/i18n/locales/pl.json +65 -7
- package/i18n/locales/tr.json +65 -7
- package/i18n/locales/uk.json +65 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -78,9 +78,10 @@ example ships in [`deploy/frontend`](../../deploy/frontend) (the `acme:security`
|
|
|
78
78
|
docs/issues/scenarios. Decisions resolve via `DecisionModal`.
|
|
79
79
|
- **Pipeline builder** (`components/pipeline`) — assemble/edit agent chains and
|
|
80
80
|
watch `PipelineProgress`.
|
|
81
|
-
- **Integrations** — modals/panels for `github
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
- **Integrations** — modals/panels for `github` (the source-control panel, shared
|
|
82
|
+
by every VCS provider), `vcs` (the GitLab personal-access-token connect),
|
|
83
|
+
`bootstrap`, `documents`, `tasks`, `requirements` (review), `scenarios`
|
|
84
|
+
(acceptance), and `fragments` (the prompt-fragment library).
|
|
84
85
|
- **Auth** (`components/auth`) — `AuthGate` / `LoginScreen` / `UserMenu`; the app
|
|
85
86
|
is gated when the backend requires sign-in.
|
|
86
87
|
|
|
@@ -2,28 +2,24 @@
|
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
4
4
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
5
|
+
import type { VcsProvider } from '~/types/domain'
|
|
6
|
+
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS, VCS_PROVIDER_TOKEN_URLS } from '~/utils/vcs'
|
|
5
7
|
|
|
6
8
|
const auth = useAuthStore()
|
|
7
9
|
const { t } = useI18n()
|
|
8
10
|
|
|
9
11
|
// Local-mode source-control PAT login. The PAT lives server-side in env (GITHUB_PAT /
|
|
10
12
|
// GITLAB_PAT); the login screen only SELECTS a configured provider — no token is ever typed
|
|
11
|
-
// into or shown in the browser.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// the server's scopes-preselected deep link (`patLogin.setupUrls`);
|
|
15
|
-
|
|
13
|
+
// into or shown in the browser. The brand labels / icons / token-settings URLs are the shared
|
|
14
|
+
// provider descriptors in `~/utils/vcs` (brand names stay verbatim across locales, so they are
|
|
15
|
+
// constants rather than catalog keys — the same convention as ApiKeysSection). The "create a
|
|
16
|
+
// token" link prefers the server's scopes-preselected deep link (`patLogin.setupUrls`); the
|
|
17
|
+
// descriptor's URL is the fallback.
|
|
18
|
+
type PatProvider = VcsProvider
|
|
16
19
|
const ALL_PROVIDERS: PatProvider[] = ['github', 'gitlab']
|
|
17
|
-
const PROVIDER_LABELS
|
|
18
|
-
const PROVIDER_ICONS
|
|
19
|
-
|
|
20
|
-
gitlab: 'i-lucide-gitlab',
|
|
21
|
-
}
|
|
22
|
-
// Fallback token-creation pages, used only if the server didn't advertise a deep link.
|
|
23
|
-
const PROVIDER_TOKEN_URLS: Record<PatProvider, string> = {
|
|
24
|
-
github: 'https://github.com/settings/tokens/new',
|
|
25
|
-
gitlab: 'https://gitlab.com/-/user_settings/personal_access_tokens',
|
|
26
|
-
}
|
|
20
|
+
const PROVIDER_LABELS = VCS_PROVIDER_LABELS
|
|
21
|
+
const PROVIDER_ICONS = VCS_PROVIDER_ICONS
|
|
22
|
+
const PROVIDER_TOKEN_URLS = VCS_PROVIDER_TOKEN_URLS
|
|
27
23
|
|
|
28
24
|
const patLoginCfg = computed(() => auth.localMode?.patLogin)
|
|
29
25
|
// Only providers whose PAT is configured in env can sign in (the token is the operational
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Hard onboarding gate shown after login when the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// the
|
|
6
|
-
// account-level install (https://github.com/apps/<slug>/installations/new — the
|
|
2
|
+
// Hard onboarding gate shown after login when the VCS integration is enabled but the workspace
|
|
3
|
+
// has no connection yet. cat-factory's whole flow runs on a connected repository host (agents
|
|
4
|
+
// open pull/merge requests on the user's repos), so the board is withheld until the workspace
|
|
5
|
+
// connects one. Renders the connect surfaces the deployment can actually serve: <GitHubConnect>
|
|
6
|
+
// drives the account-level App install (https://github.com/apps/<slug>/installations/new — the
|
|
7
7
|
// user picks the account/org and grants all or a subset of repos) plus the
|
|
8
|
-
// pick-an-existing-installation path
|
|
9
|
-
// user who needs to switch
|
|
8
|
+
// pick-an-existing-installation path, and <GitLabConnect> takes a pasted GitLab PAT. A "Sign
|
|
9
|
+
// out" escape hatch avoids trapping a user who needs to switch accounts.
|
|
10
10
|
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
11
|
+
import GitLabConnect from '~/components/vcs/GitLabConnect.vue'
|
|
12
|
+
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
11
13
|
|
|
12
14
|
const { t } = useI18n()
|
|
13
15
|
const auth = useAuthStore()
|
|
16
|
+
const github = useGitHubStore()
|
|
17
|
+
|
|
18
|
+
// A deployment that serves exactly one provider names it; one serving several stays neutral
|
|
19
|
+
// (the per-surface copy below then says which is which).
|
|
20
|
+
const sole = computed(() => github.soleConnectProvider)
|
|
21
|
+
const icon = computed(() => (sole.value ? VCS_PROVIDER_ICONS[sole.value] : 'i-lucide-git-branch'))
|
|
22
|
+
const title = computed(() =>
|
|
23
|
+
sole.value
|
|
24
|
+
? t('vcs.onboarding.title', { provider: VCS_PROVIDER_LABELS[sole.value] })
|
|
25
|
+
: t('vcs.onboarding.titleAny'),
|
|
26
|
+
)
|
|
14
27
|
</script>
|
|
15
28
|
|
|
16
29
|
<template>
|
|
@@ -21,14 +34,29 @@ const auth = useAuthStore()
|
|
|
21
34
|
class="my-8 w-full max-w-md rounded-xl border border-slate-800 bg-slate-900/80 p-8 backdrop-blur"
|
|
22
35
|
>
|
|
23
36
|
<div class="mb-5 text-center">
|
|
24
|
-
<UIcon name="
|
|
25
|
-
<h1 class="mb-1 text-lg font-semibold text-white">{{
|
|
37
|
+
<UIcon :name="icon" class="mx-auto mb-3 h-10 w-10 text-indigo-400" />
|
|
38
|
+
<h1 class="mb-1 text-lg font-semibold text-white">{{ title }}</h1>
|
|
26
39
|
<p class="text-sm text-slate-400">
|
|
27
|
-
{{ t('
|
|
40
|
+
{{ t('vcs.onboarding.intro') }}
|
|
28
41
|
</p>
|
|
29
42
|
</div>
|
|
30
43
|
|
|
31
|
-
<
|
|
44
|
+
<template v-if="github.canConnectGitHubApp">
|
|
45
|
+
<p class="mb-3 text-sm text-slate-400">{{ t('github.onboarding.appIntro') }}</p>
|
|
46
|
+
<GitHubConnect />
|
|
47
|
+
</template>
|
|
48
|
+
<USeparator
|
|
49
|
+
v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
|
|
50
|
+
class="my-4"
|
|
51
|
+
:label="t('vcs.connect.or')"
|
|
52
|
+
/>
|
|
53
|
+
<GitLabConnect v-if="github.canConnectGitLabPat" />
|
|
54
|
+
<p
|
|
55
|
+
v-if="!github.canConnectGitHubApp && !github.canConnectGitLabPat"
|
|
56
|
+
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
57
|
+
>
|
|
58
|
+
{{ t('vcs.connect.noneConfigured') }}
|
|
59
|
+
</p>
|
|
32
60
|
|
|
33
61
|
<p
|
|
34
62
|
v-if="auth.required && auth.user"
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
// pull requests and issues the backend caches in D1. Mirrors the document-source
|
|
5
5
|
// connect/import surface, but for GitHub. Writes (new branch, open/merge PR,
|
|
6
6
|
// comment) go straight to the repo via the backend's installation token.
|
|
7
|
-
import type { GitHubPullRequest, GitHubRepo } from '~/types/domain'
|
|
7
|
+
import type { GitHubPullRequest, GitHubRepo, VcsProvider } from '~/types/domain'
|
|
8
8
|
// Explicit import: the auto-import name for a component nested under a
|
|
9
9
|
// like-named directory (github/GitHubConnect) doesn't match the `<GitHubConnect>`
|
|
10
10
|
// tag, so it silently renders as an empty element. Importing it directly binds
|
|
11
11
|
// the tag unambiguously.
|
|
12
12
|
import GitHubConnect from './GitHubConnect.vue'
|
|
13
|
+
import GitLabConnect from '~/components/vcs/GitLabConnect.vue'
|
|
13
14
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
15
|
+
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
14
16
|
|
|
15
17
|
const { t } = useI18n()
|
|
16
18
|
const ui = useUiStore()
|
|
@@ -26,6 +28,32 @@ const open = computed({
|
|
|
26
28
|
})
|
|
27
29
|
const back = useIntegrationBack(open)
|
|
28
30
|
|
|
31
|
+
// Provider-aware chrome: once connected, the panel is titled for the provider the workspace
|
|
32
|
+
// actually connected; before that it names the sole connectable provider, or stays neutral when
|
|
33
|
+
// the deployment serves several. Brand names are verbatim in every locale (see `~/utils/vcs`).
|
|
34
|
+
const chromeProvider = computed(() =>
|
|
35
|
+
github.connected ? github.provider : github.soleConnectProvider,
|
|
36
|
+
)
|
|
37
|
+
const panelTitle = computed(() =>
|
|
38
|
+
chromeProvider.value ? VCS_PROVIDER_LABELS[chromeProvider.value] : t('vcs.panel.title'),
|
|
39
|
+
)
|
|
40
|
+
const providerIcon = computed(() =>
|
|
41
|
+
chromeProvider.value ? VCS_PROVIDER_ICONS[chromeProvider.value] : 'i-lucide-git-branch',
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// What the connection line says under the account: a GitHub App connection is identified by its
|
|
45
|
+
// installation, a PAT connection by the credential it rides. Keyed by an exhaustive Record of
|
|
46
|
+
// literal `t()` keys, so a new provider fails the typecheck rather than rendering App vocabulary.
|
|
47
|
+
const CONNECTION_META: Record<VcsProvider, () => string> = {
|
|
48
|
+
github: () =>
|
|
49
|
+
t('github.panel.installationMeta', {
|
|
50
|
+
targetType: github.connection?.targetType ?? '',
|
|
51
|
+
id: github.connection?.installationId ?? '',
|
|
52
|
+
}),
|
|
53
|
+
gitlab: () => t('vcs.panel.patMeta'),
|
|
54
|
+
}
|
|
55
|
+
const connectionMeta = computed(() => CONNECTION_META[github.provider]())
|
|
56
|
+
|
|
29
57
|
// On open: refresh projections when connected. The not-connected state renders
|
|
30
58
|
// <GitHubConnect>, which discovers and links installations on its own.
|
|
31
59
|
watch(
|
|
@@ -48,8 +76,10 @@ function notifyError(title: string, e: unknown) {
|
|
|
48
76
|
|
|
49
77
|
async function disconnect() {
|
|
50
78
|
const ok = await confirm({
|
|
51
|
-
title: t('
|
|
52
|
-
|
|
79
|
+
title: t('vcs.panel.confirmDisconnect.title', {
|
|
80
|
+
provider: VCS_PROVIDER_LABELS[github.provider],
|
|
81
|
+
}),
|
|
82
|
+
description: t('vcs.panel.confirmDisconnect.body'),
|
|
53
83
|
variant: 'destructive',
|
|
54
84
|
confirmLabel: t('common.disconnect'),
|
|
55
85
|
icon: 'i-lucide-unplug',
|
|
@@ -57,7 +87,10 @@ async function disconnect() {
|
|
|
57
87
|
if (!ok) return
|
|
58
88
|
try {
|
|
59
89
|
await github.disconnect()
|
|
60
|
-
toast.add({
|
|
90
|
+
toast.add({
|
|
91
|
+
title: t('vcs.panel.toast.disconnected', { provider: VCS_PROVIDER_LABELS[github.provider] }),
|
|
92
|
+
icon: 'i-lucide-unplug',
|
|
93
|
+
})
|
|
61
94
|
} catch (e) {
|
|
62
95
|
notifyError(t('github.panel.errors.disconnect'), e)
|
|
63
96
|
}
|
|
@@ -241,19 +274,32 @@ async function merge(pr: GitHubPullRequest) {
|
|
|
241
274
|
</script>
|
|
242
275
|
|
|
243
276
|
<template>
|
|
244
|
-
<UModal v-model:open="open" title="
|
|
277
|
+
<UModal v-model:open="open" :title="panelTitle" :ui="{ content: 'max-w-2xl' }">
|
|
245
278
|
<template #title>
|
|
246
|
-
<IntegrationBackTitle title="
|
|
279
|
+
<IntegrationBackTitle :title="panelTitle" @back="back" />
|
|
247
280
|
</template>
|
|
248
281
|
<template #body>
|
|
249
282
|
<div class="space-y-5">
|
|
250
283
|
<!-- not connected: connect -->
|
|
251
284
|
<template v-if="!github.connected">
|
|
252
|
-
|
|
253
|
-
|
|
285
|
+
<!-- One connect surface per method the deployment actually serves: the App
|
|
286
|
+
installation picker only where a GitHub App is configured, the PAT box only
|
|
287
|
+
where the per-workspace GitLab connect is wired. -->
|
|
288
|
+
<template v-if="github.canConnectGitHubApp">
|
|
289
|
+
<p class="text-sm text-slate-400">{{ t('github.panel.connectIntro') }}</p>
|
|
290
|
+
<GitHubConnect />
|
|
291
|
+
</template>
|
|
292
|
+
<USeparator
|
|
293
|
+
v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
|
|
294
|
+
:label="t('vcs.connect.or')"
|
|
295
|
+
/>
|
|
296
|
+
<GitLabConnect v-if="github.canConnectGitLabPat" />
|
|
297
|
+
<p
|
|
298
|
+
v-if="!github.canConnectGitHubApp && !github.canConnectGitLabPat"
|
|
299
|
+
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
300
|
+
>
|
|
301
|
+
{{ t('vcs.connect.noneConfigured') }}
|
|
254
302
|
</p>
|
|
255
|
-
|
|
256
|
-
<GitHubConnect />
|
|
257
303
|
</template>
|
|
258
304
|
|
|
259
305
|
<!-- connected: manage + browse -->
|
|
@@ -263,19 +309,12 @@ async function merge(pr: GitHubPullRequest) {
|
|
|
263
309
|
class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2"
|
|
264
310
|
>
|
|
265
311
|
<div class="flex items-center gap-2 min-w-0">
|
|
266
|
-
<UIcon name="
|
|
312
|
+
<UIcon :name="providerIcon" class="h-5 w-5 text-slate-300" />
|
|
267
313
|
<div class="min-w-0">
|
|
268
314
|
<div class="truncate text-sm text-slate-200">
|
|
269
315
|
{{ github.connection?.accountLogin }}
|
|
270
316
|
</div>
|
|
271
|
-
<div class="text-[11px] text-slate-500">
|
|
272
|
-
{{
|
|
273
|
-
t('github.panel.installationMeta', {
|
|
274
|
-
targetType: github.connection?.targetType,
|
|
275
|
-
id: github.connection?.installationId,
|
|
276
|
-
})
|
|
277
|
-
}}
|
|
278
|
-
</div>
|
|
317
|
+
<div class="text-[11px] text-slate-500">{{ connectionMeta }}</div>
|
|
279
318
|
</div>
|
|
280
319
|
</div>
|
|
281
320
|
<div class="flex items-center gap-1">
|
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
//
|
|
21
21
|
// It also owns the one trailing section every step-backed window shows: the agent's effort
|
|
22
22
|
// self-assessment (see the footer block below), so a window never renders it itself.
|
|
23
|
-
import { computed, ref } from 'vue'
|
|
23
|
+
import { computed, ref, watch } from 'vue'
|
|
24
24
|
import { useModalBehavior } from '@modular-vue/core'
|
|
25
25
|
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
26
26
|
import StepEffortReport from '~/components/panels/StepEffortReport.vue'
|
|
27
|
+
import StepValidationReport from '~/components/panels/StepValidationReport.vue'
|
|
27
28
|
import { effortBand, effortHint } from '~/utils/effort'
|
|
28
29
|
|
|
29
30
|
/** A pipeline step reference — passed by step-result windows to surface the shared
|
|
@@ -85,11 +86,26 @@ const { dialogRef } = useModalBehavior({
|
|
|
85
86
|
// step whose agent wrote no report both resolve to null, and the footer disappears.
|
|
86
87
|
const ui = useUiStore()
|
|
87
88
|
const execution = useExecutionStore()
|
|
88
|
-
const
|
|
89
|
+
const activeStep = computed(() => {
|
|
89
90
|
const view = ui.resultView
|
|
90
91
|
if (!view || view.instanceId === null || view.stepIndex === null) return null
|
|
91
|
-
return execution.getInstance(view.instanceId)?.steps[view.stepIndex]
|
|
92
|
+
return execution.getInstance(view.instanceId)?.steps[view.stepIndex] ?? null
|
|
92
93
|
})
|
|
94
|
+
const effortReport = computed(() => activeStep.value?.effortReport ?? null)
|
|
95
|
+
// The step's PRE-PR VALIDATION report — the second universal trailing section, resolved the same
|
|
96
|
+
// way and for the same reason: every window whose step ran a coding job can show whether the
|
|
97
|
+
// checkout actually passed the service's checks before the PR opened (and, on a red run, exactly
|
|
98
|
+
// what failed). Absent for a service that configured no checks, and the footer disappears.
|
|
99
|
+
const validationReport = computed(() => activeStep.value?.validation ?? null)
|
|
100
|
+
const validationOpen = ref(false)
|
|
101
|
+
// A failing report is the interesting one, so it opens expanded; a green one stays a one-line row.
|
|
102
|
+
watch(
|
|
103
|
+
validationReport,
|
|
104
|
+
(report) => {
|
|
105
|
+
if (report && !report.passed) validationOpen.value = true
|
|
106
|
+
},
|
|
107
|
+
{ immediate: true },
|
|
108
|
+
)
|
|
93
109
|
// Collapsed by default — the windows own the vertical space, and the row already carries the
|
|
94
110
|
// difficulty plus the gist of what held the agent back.
|
|
95
111
|
const effortOpen = ref(false)
|
|
@@ -204,6 +220,61 @@ const panelClass = computed(() => [
|
|
|
204
220
|
<StepEffortReport :report="effortReport" variant="flat" />
|
|
205
221
|
</div>
|
|
206
222
|
</section>
|
|
223
|
+
|
|
224
|
+
<!-- Shared trailing section: the pre-PR validation report (the service's check commands
|
|
225
|
+
run against the checkout BEFORE the PR opened). Collapsed when green, expanded when
|
|
226
|
+
red — a failed checkout is the one an operator opened the window to read. -->
|
|
227
|
+
<section
|
|
228
|
+
v-if="validationReport"
|
|
229
|
+
class="shrink-0 border-t border-slate-800 bg-slate-900/60"
|
|
230
|
+
data-testid="result-window-validation"
|
|
231
|
+
>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-slate-800/40"
|
|
235
|
+
:aria-expanded="validationOpen"
|
|
236
|
+
data-testid="result-window-validation-toggle"
|
|
237
|
+
@click="validationOpen = !validationOpen"
|
|
238
|
+
>
|
|
239
|
+
<UIcon
|
|
240
|
+
:name="validationReport.passed ? 'i-lucide-shield-check' : 'i-lucide-shield-alert'"
|
|
241
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
242
|
+
:class="validationReport.passed ? 'text-emerald-400' : 'text-rose-400'"
|
|
243
|
+
/>
|
|
244
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
245
|
+
{{ t('panels.stepDetail.validation.heading') }}
|
|
246
|
+
</span>
|
|
247
|
+
<span
|
|
248
|
+
class="shrink-0 rounded px-1.5 py-0.5 text-[11px] font-medium tabular-nums"
|
|
249
|
+
:class="
|
|
250
|
+
validationReport.passed
|
|
251
|
+
? 'bg-emerald-500/15 text-emerald-300'
|
|
252
|
+
: 'bg-rose-500/15 text-rose-300'
|
|
253
|
+
"
|
|
254
|
+
>
|
|
255
|
+
{{
|
|
256
|
+
validationReport.passed
|
|
257
|
+
? t('panels.stepDetail.validation.passed')
|
|
258
|
+
: t('panels.stepDetail.validation.failed')
|
|
259
|
+
}}
|
|
260
|
+
</span>
|
|
261
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-400">
|
|
262
|
+
{{
|
|
263
|
+
t('panels.stepDetail.validation.attempts', {
|
|
264
|
+
attempts: validationReport.attempts,
|
|
265
|
+
maxAttempts: validationReport.maxAttempts,
|
|
266
|
+
})
|
|
267
|
+
}}
|
|
268
|
+
</span>
|
|
269
|
+
<UIcon
|
|
270
|
+
:name="validationOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
|
|
271
|
+
class="ms-auto h-3.5 w-3.5 shrink-0 text-slate-500"
|
|
272
|
+
/>
|
|
273
|
+
</button>
|
|
274
|
+
<div v-if="validationOpen" class="max-h-72 overflow-y-auto px-5 pb-3">
|
|
275
|
+
<StepValidationReport :report="validationReport" />
|
|
276
|
+
</div>
|
|
277
|
+
</section>
|
|
207
278
|
</div>
|
|
208
279
|
</div>
|
|
209
280
|
</Teleport>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { ValidationReport } from '~/types/validationChecks'
|
|
4
|
+
|
|
5
|
+
// The PRE-PR VALIDATION report for a step: which of the service's configured check commands
|
|
6
|
+
// passed against the checkout before the PR was allowed to open, their exit codes, and a
|
|
7
|
+
// bounded, secret-scrubbed tail of each command's output. Produced by the executor-harness
|
|
8
|
+
// running the commands — never a model self-report — so this is the run's captured PROOF on a
|
|
9
|
+
// green run and its EVIDENCE on a failed one. See docs/initiatives/pre-pr-validation.md.
|
|
10
|
+
const props = defineProps<{ report: ValidationReport }>()
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
const failed = computed(() => props.report.outcomes.filter((o) => !o.passed))
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div class="space-y-2" data-testid="step-validation-report">
|
|
18
|
+
<p class="text-[11px] text-slate-400">
|
|
19
|
+
{{
|
|
20
|
+
report.passed
|
|
21
|
+
? t('panels.stepDetail.validation.passedSummary', {
|
|
22
|
+
total: report.outcomes.length,
|
|
23
|
+
attempts: report.attempts,
|
|
24
|
+
})
|
|
25
|
+
: t('panels.stepDetail.validation.failedSummary', {
|
|
26
|
+
failed: failed.length,
|
|
27
|
+
total: report.outcomes.length,
|
|
28
|
+
attempts: report.attempts,
|
|
29
|
+
maxAttempts: report.maxAttempts,
|
|
30
|
+
})
|
|
31
|
+
}}
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
v-for="outcome in report.outcomes"
|
|
36
|
+
:key="outcome.label"
|
|
37
|
+
class="rounded-md border border-slate-800 bg-slate-950/40 p-2"
|
|
38
|
+
data-testid="validation-outcome"
|
|
39
|
+
>
|
|
40
|
+
<div class="flex items-center gap-2">
|
|
41
|
+
<UIcon
|
|
42
|
+
:name="outcome.passed ? 'i-lucide-check' : 'i-lucide-x'"
|
|
43
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
44
|
+
:class="outcome.passed ? 'text-emerald-400' : 'text-rose-400'"
|
|
45
|
+
/>
|
|
46
|
+
<span class="text-[12px] font-medium text-slate-200">{{ outcome.label }}</span>
|
|
47
|
+
<span class="truncate font-mono text-[11px] text-slate-500">{{ outcome.command }}</span>
|
|
48
|
+
<span
|
|
49
|
+
v-if="!outcome.passed"
|
|
50
|
+
class="ms-auto shrink-0 rounded bg-rose-500/15 px-1.5 py-0.5 text-[11px] tabular-nums text-rose-300"
|
|
51
|
+
>
|
|
52
|
+
{{
|
|
53
|
+
outcome.timedOut
|
|
54
|
+
? t('panels.stepDetail.validation.timedOut')
|
|
55
|
+
: t('panels.stepDetail.validation.exitCode', { code: outcome.exitCode })
|
|
56
|
+
}}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<pre
|
|
60
|
+
v-if="outcome.outputTail"
|
|
61
|
+
class="mt-1.5 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-slate-950 p-2 font-mono text-[11px] text-slate-400"
|
|
62
|
+
data-testid="validation-output"
|
|
63
|
+
>{{ outcome.outputTail }}</pre>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import type { Block } from '~/types/domain'
|
|
4
|
+
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
+
import {
|
|
6
|
+
VALIDATION_DEFAULT_MAX_ATTEMPTS,
|
|
7
|
+
VALIDATION_MAX_ATTEMPTS_CEILING,
|
|
8
|
+
VALIDATION_MAX_CHECKS,
|
|
9
|
+
type ValidationCheck,
|
|
10
|
+
} from '~/types/validationChecks'
|
|
11
|
+
|
|
12
|
+
// Per-service (frame) PRE-PR VALIDATION CHECKS: the shell commands the executor-harness runs
|
|
13
|
+
// against the checkout after the coder settles and BEFORE the PR opens. A failing command's
|
|
14
|
+
// output is handed back to the agent, which retries under the attempt budget; only a green
|
|
15
|
+
// checkout opens a PR, and an exhausted budget fails the step with the captured output.
|
|
16
|
+
// Keyed by THIS block's id (service frames only — a task resolves its checks up the frame chain).
|
|
17
|
+
const props = defineProps<{ block: Block }>()
|
|
18
|
+
|
|
19
|
+
const store = useValidationChecksStore()
|
|
20
|
+
const toast = useToast()
|
|
21
|
+
const { t } = useI18n()
|
|
22
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
23
|
+
|
|
24
|
+
const busy = ref(false)
|
|
25
|
+
const rows = ref<ValidationCheck[]>([])
|
|
26
|
+
const maxAttempts = ref(VALIDATION_DEFAULT_MAX_ATTEMPTS)
|
|
27
|
+
|
|
28
|
+
const saved = computed(() => store.forBlock(props.block.id))
|
|
29
|
+
const configured = computed(() => saved.value.checks.length > 0)
|
|
30
|
+
const canAdd = computed(() => rows.value.length < VALIDATION_MAX_CHECKS)
|
|
31
|
+
/** A row is only submittable once it has a command; the label falls back to the command. */
|
|
32
|
+
const submittable = computed(() =>
|
|
33
|
+
rows.value
|
|
34
|
+
.map((r) => ({ label: r.label.trim() || r.command.trim(), command: r.command.trim() }))
|
|
35
|
+
.filter((r) => r.command.length > 0),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
onMounted(() => {
|
|
39
|
+
store.ensureLoaded().catch(() => {})
|
|
40
|
+
})
|
|
41
|
+
watch(
|
|
42
|
+
saved,
|
|
43
|
+
(config) => {
|
|
44
|
+
rows.value = config.checks.map((c) => ({ ...c }))
|
|
45
|
+
maxAttempts.value = config.maxAttempts
|
|
46
|
+
},
|
|
47
|
+
{ immediate: true },
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
function addRow() {
|
|
51
|
+
if (canAdd.value) rows.value.push({ label: '', command: '' })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function removeRow(index: number) {
|
|
55
|
+
rows.value.splice(index, 1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function notifyError(title: string, e: unknown) {
|
|
59
|
+
toast.add({
|
|
60
|
+
title,
|
|
61
|
+
description: e instanceof Error ? e.message : String(e),
|
|
62
|
+
icon: 'i-lucide-triangle-alert',
|
|
63
|
+
color: 'error',
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function save() {
|
|
68
|
+
busy.value = true
|
|
69
|
+
try {
|
|
70
|
+
await store.save(props.block.id, submittable.value, maxAttempts.value)
|
|
71
|
+
toast.add({
|
|
72
|
+
title: t('inspector.validationChecks.savedToast'),
|
|
73
|
+
icon: 'i-lucide-check',
|
|
74
|
+
color: 'success',
|
|
75
|
+
})
|
|
76
|
+
} catch (e) {
|
|
77
|
+
notifyError(t('inspector.validationChecks.saveFailed'), e)
|
|
78
|
+
} finally {
|
|
79
|
+
busy.value = false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function clear() {
|
|
84
|
+
const noun = t('inspector.validationChecks.configNoun')
|
|
85
|
+
if (!(await confirmAction('clear', noun))) return
|
|
86
|
+
busy.value = true
|
|
87
|
+
try {
|
|
88
|
+
await store.remove(props.block.id)
|
|
89
|
+
rows.value = []
|
|
90
|
+
toastDone('clear', noun)
|
|
91
|
+
} catch (e) {
|
|
92
|
+
notifyError(t('inspector.validationChecks.clearFailed'), e)
|
|
93
|
+
} finally {
|
|
94
|
+
busy.value = false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<InspectorSection
|
|
101
|
+
v-if="store.available !== false"
|
|
102
|
+
:title="t('inspector.validationChecks.title')"
|
|
103
|
+
:hint="t('inspector.validationChecks.sectionHint')"
|
|
104
|
+
data-testid="service-validation-config"
|
|
105
|
+
>
|
|
106
|
+
<template #actions>
|
|
107
|
+
<UButton
|
|
108
|
+
v-if="configured"
|
|
109
|
+
color="error"
|
|
110
|
+
variant="ghost"
|
|
111
|
+
size="xs"
|
|
112
|
+
icon="i-lucide-trash-2"
|
|
113
|
+
:loading="busy"
|
|
114
|
+
data-testid="validation-clear"
|
|
115
|
+
@click="clear"
|
|
116
|
+
>
|
|
117
|
+
{{ t('inspector.validationChecks.clear') }}
|
|
118
|
+
</UButton>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<div class="space-y-2">
|
|
122
|
+
<p class="text-[11px] text-slate-500">
|
|
123
|
+
{{ t('inspector.validationChecks.hint') }}
|
|
124
|
+
</p>
|
|
125
|
+
|
|
126
|
+
<div
|
|
127
|
+
v-for="(row, index) in rows"
|
|
128
|
+
:key="index"
|
|
129
|
+
class="flex items-end gap-2"
|
|
130
|
+
data-testid="validation-check-row"
|
|
131
|
+
>
|
|
132
|
+
<UFormField :label="t('inspector.validationChecks.label')" class="w-1/3">
|
|
133
|
+
<UInput
|
|
134
|
+
v-model="row.label"
|
|
135
|
+
:placeholder="t('inspector.validationChecks.labelPlaceholder')"
|
|
136
|
+
size="sm"
|
|
137
|
+
class="w-full"
|
|
138
|
+
data-testid="validation-check-label"
|
|
139
|
+
/>
|
|
140
|
+
</UFormField>
|
|
141
|
+
<UFormField :label="t('inspector.validationChecks.command')" class="flex-1">
|
|
142
|
+
<UInput
|
|
143
|
+
v-model="row.command"
|
|
144
|
+
placeholder="pnpm lint"
|
|
145
|
+
size="sm"
|
|
146
|
+
class="w-full"
|
|
147
|
+
data-testid="validation-check-command"
|
|
148
|
+
/>
|
|
149
|
+
</UFormField>
|
|
150
|
+
<UButton
|
|
151
|
+
color="neutral"
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="xs"
|
|
154
|
+
icon="i-lucide-x"
|
|
155
|
+
:aria-label="t('inspector.validationChecks.removeCheck')"
|
|
156
|
+
data-testid="validation-check-remove"
|
|
157
|
+
@click="removeRow(index)"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<p v-if="rows.length === 0" class="text-[11px] text-slate-500" data-testid="validation-empty">
|
|
162
|
+
{{ t('inspector.validationChecks.empty') }}
|
|
163
|
+
</p>
|
|
164
|
+
|
|
165
|
+
<div class="flex items-end justify-between gap-2">
|
|
166
|
+
<UFormField
|
|
167
|
+
:label="t('inspector.validationChecks.maxAttempts')"
|
|
168
|
+
:hint="t('inspector.validationChecks.maxAttemptsHint')"
|
|
169
|
+
class="w-40"
|
|
170
|
+
>
|
|
171
|
+
<UInput
|
|
172
|
+
v-model.number="maxAttempts"
|
|
173
|
+
type="number"
|
|
174
|
+
:min="1"
|
|
175
|
+
:max="VALIDATION_MAX_ATTEMPTS_CEILING"
|
|
176
|
+
size="sm"
|
|
177
|
+
class="w-full"
|
|
178
|
+
data-testid="validation-max-attempts"
|
|
179
|
+
/>
|
|
180
|
+
</UFormField>
|
|
181
|
+
<div class="flex gap-2">
|
|
182
|
+
<UButton
|
|
183
|
+
color="neutral"
|
|
184
|
+
variant="soft"
|
|
185
|
+
size="xs"
|
|
186
|
+
icon="i-lucide-plus"
|
|
187
|
+
:disabled="!canAdd"
|
|
188
|
+
data-testid="validation-add-check"
|
|
189
|
+
@click="addRow"
|
|
190
|
+
>
|
|
191
|
+
{{ t('inspector.validationChecks.addCheck') }}
|
|
192
|
+
</UButton>
|
|
193
|
+
<UButton
|
|
194
|
+
color="primary"
|
|
195
|
+
variant="soft"
|
|
196
|
+
size="xs"
|
|
197
|
+
icon="i-lucide-save"
|
|
198
|
+
:loading="busy"
|
|
199
|
+
data-testid="validation-save"
|
|
200
|
+
@click="save"
|
|
201
|
+
>
|
|
202
|
+
{{ t('inspector.validationChecks.save') }}
|
|
203
|
+
</UButton>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</InspectorSection>
|
|
208
|
+
</template>
|