@cat-factory/app 0.47.6 → 0.47.8
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 +41 -69
- package/app/components/gates/GateResultView.vue +91 -57
- package/app/components/palettes/AgentPalette.vue +4 -2
- package/app/components/pipeline/IterationCapPrompt.vue +13 -10
- package/app/components/pipeline/PipelineBuilder.vue +112 -70
- package/app/components/pipeline/PipelineHealthModal.vue +20 -16
- package/app/components/pipeline/PipelineProgress.vue +79 -39
- package/app/stores/auth.ts +41 -2
- package/i18n/locales/en.json +216 -5
- package/i18n/locales/es.json +216 -5
- package/i18n/locales/fr.json +216 -5
- package/i18n/locales/pl.json +216 -5
- package/i18n/locales/uk.json +216 -5
- package/package.json +2 -2
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
3
|
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
4
4
|
|
|
5
5
|
const auth = useAuthStore()
|
|
6
6
|
const { t } = useI18n()
|
|
7
7
|
|
|
8
|
-
// Local-mode source-control PAT login.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// Local-mode source-control PAT login. The PAT lives server-side in env (GITHUB_PAT /
|
|
9
|
+
// GITLAB_PAT); the login screen only SELECTS a configured provider — no token is ever typed
|
|
10
|
+
// into or shown in the browser. GitHub/GitLab are brand names (kept verbatim across locales),
|
|
11
|
+
// as are the token-settings URLs, so they're inline constants rather than catalog keys — same
|
|
12
|
+
// convention as the provider descriptors in ApiKeysSection. The "create a token" link prefers
|
|
13
|
+
// the server's scopes-preselected deep link (`patLogin.setupUrls`); these are the fallback.
|
|
13
14
|
type PatProvider = 'github' | 'gitlab'
|
|
15
|
+
const ALL_PROVIDERS: PatProvider[] = ['github', 'gitlab']
|
|
14
16
|
const PROVIDER_LABELS: Record<PatProvider, string> = { github: 'GitHub', gitlab: 'GitLab' }
|
|
15
17
|
const PROVIDER_ICONS: Record<PatProvider, string> = {
|
|
16
18
|
github: 'i-lucide-github',
|
|
@@ -23,44 +25,29 @@ const PROVIDER_TOKEN_URLS: Record<PatProvider, string> = {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const patLoginCfg = computed(() => auth.localMode?.patLogin)
|
|
28
|
+
// Only providers whose PAT is configured in env can sign in (the token is the operational
|
|
29
|
+
// credential too). A provider without one gets no button — see the no-PAT notice instead.
|
|
26
30
|
const configuredProviders = computed<PatProvider[]>(
|
|
27
31
|
() => (patLoginCfg.value?.configured ?? []) as PatProvider[],
|
|
28
32
|
)
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
const showLocalLogin = computed(() => availableProviders.value.length > 0)
|
|
33
|
+
const isLocalMode = computed(() => auth.localMode?.enabled === true)
|
|
34
|
+
const hasConfiguredPat = computed(() => configuredProviders.value.length > 0)
|
|
33
35
|
|
|
34
|
-
const patProvider = ref<PatProvider>('github')
|
|
35
|
-
const patToken = ref('')
|
|
36
36
|
const patBusy = ref(false)
|
|
37
37
|
const patError = ref<string | null>(null)
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
{ immediate: true },
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
const patProviderItems = computed(() =>
|
|
49
|
-
availableProviders.value.map((p) => ({ label: PROVIDER_LABELS[p], value: p })),
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
// Prefer the server's scopes-preselected deep link (it owns the per-provider scopes);
|
|
53
|
-
// fall back to the plain token page if it wasn't advertised.
|
|
54
|
-
const tokenCreateUrl = computed(
|
|
55
|
-
() => patLoginCfg.value?.setupUrls?.[patProvider.value] ?? PROVIDER_TOKEN_URLS[patProvider.value],
|
|
56
|
-
)
|
|
39
|
+
// Per-provider "create a token" link: prefer the server's scopes-preselected deep link (it
|
|
40
|
+
// owns the per-provider scopes), fall back to the plain token page.
|
|
41
|
+
function tokenCreateUrl(provider: PatProvider): string {
|
|
42
|
+
return patLoginCfg.value?.setupUrls?.[provider] ?? PROVIDER_TOKEN_URLS[provider]
|
|
43
|
+
}
|
|
57
44
|
|
|
58
|
-
/**
|
|
59
|
-
async function submitPat(provider: PatProvider
|
|
45
|
+
/** Sign in as the account the configured env PAT belongs to; reloads so the app boots in. */
|
|
46
|
+
async function submitPat(provider: PatProvider) {
|
|
60
47
|
patError.value = null
|
|
61
48
|
patBusy.value = true
|
|
62
49
|
try {
|
|
63
|
-
await auth.patLogin(
|
|
50
|
+
await auth.patLogin({ provider })
|
|
64
51
|
if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
|
|
65
52
|
} catch (e) {
|
|
66
53
|
patError.value = apiErrorEnvelope(e)?.message ?? t('auth.localMode.failed')
|
|
@@ -154,9 +141,10 @@ const showOAuthDivider = computed(
|
|
|
154
141
|
</p>
|
|
155
142
|
</div>
|
|
156
143
|
|
|
157
|
-
<!-- Local mode: sign in with
|
|
158
|
-
|
|
159
|
-
|
|
144
|
+
<!-- Local mode: sign in with the env-configured source-control PAT. The token lives
|
|
145
|
+
server-side (GITHUB_PAT / GITLAB_PAT); we only pick a provider here. -->
|
|
146
|
+
<div v-if="isLocalMode && mode !== 'forgot'" class="space-y-3">
|
|
147
|
+
<!-- One button per provider whose PAT is configured in env -->
|
|
160
148
|
<UButton
|
|
161
149
|
v-for="p in configuredProviders"
|
|
162
150
|
:key="p"
|
|
@@ -167,53 +155,37 @@ const showOAuthDivider = computed(
|
|
|
167
155
|
:loading="patBusy"
|
|
168
156
|
@click="submitPat(p)"
|
|
169
157
|
>
|
|
170
|
-
{{ t('auth.localMode.
|
|
158
|
+
{{ t('auth.localMode.continueWithConfigured', { provider: PROVIDER_LABELS[p] }) }}
|
|
171
159
|
</UButton>
|
|
172
160
|
|
|
173
|
-
<!--
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
:
|
|
180
|
-
|
|
181
|
-
class="w-full"
|
|
182
|
-
/>
|
|
183
|
-
<UTextarea
|
|
184
|
-
v-model="patToken"
|
|
185
|
-
:rows="2"
|
|
186
|
-
:placeholder="
|
|
187
|
-
t('auth.localMode.tokenPlaceholder', { provider: PROVIDER_LABELS[patProvider] })
|
|
188
|
-
"
|
|
189
|
-
class="w-full font-mono"
|
|
161
|
+
<!-- Neither GITHUB_PAT nor GITLAB_PAT is set: tell the developer how to configure one -->
|
|
162
|
+
<template v-if="!hasConfiguredPat">
|
|
163
|
+
<UAlert
|
|
164
|
+
color="warning"
|
|
165
|
+
variant="subtle"
|
|
166
|
+
icon="i-lucide-key-round"
|
|
167
|
+
:title="t('auth.localMode.noPatTitle')"
|
|
168
|
+
:description="t('auth.localMode.noPatBody')"
|
|
190
169
|
/>
|
|
191
|
-
<div class="flex
|
|
170
|
+
<div class="flex flex-wrap gap-3 px-1">
|
|
192
171
|
<a
|
|
193
|
-
|
|
172
|
+
v-for="p in ALL_PROVIDERS"
|
|
173
|
+
:key="p"
|
|
174
|
+
:href="tokenCreateUrl(p)"
|
|
194
175
|
target="_blank"
|
|
195
176
|
rel="noopener noreferrer"
|
|
196
177
|
class="text-xs text-indigo-400 hover:underline"
|
|
197
178
|
>
|
|
198
|
-
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[
|
|
179
|
+
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[p] }) }}
|
|
199
180
|
</a>
|
|
200
|
-
<UButton
|
|
201
|
-
size="lg"
|
|
202
|
-
color="neutral"
|
|
203
|
-
variant="subtle"
|
|
204
|
-
type="submit"
|
|
205
|
-
:loading="patBusy"
|
|
206
|
-
:disabled="!patToken.trim()"
|
|
207
|
-
>
|
|
208
|
-
{{ t('auth.localMode.submit') }}
|
|
209
|
-
</UButton>
|
|
210
181
|
</div>
|
|
211
|
-
</
|
|
182
|
+
</template>
|
|
183
|
+
|
|
212
184
|
<p v-if="patError" class="text-sm text-rose-400">{{ patError }}</p>
|
|
213
185
|
</div>
|
|
214
186
|
|
|
215
187
|
<div
|
|
216
|
-
v-if="
|
|
188
|
+
v-if="isLocalMode && auth.providers.password && mode !== 'forgot'"
|
|
217
189
|
class="my-4 flex items-center gap-3 text-xs text-slate-500"
|
|
218
190
|
>
|
|
219
191
|
<span class="h-px flex-1 bg-slate-800" /> {{ t('auth.localMode.orDivider') }}
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
// gates; it branches on the step's `agentKind` for the copy and the failure detail.
|
|
8
8
|
import { computed, ref } from 'vue'
|
|
9
9
|
import { agentKindMeta } from '~/utils/catalog'
|
|
10
|
-
import type { GateStepState } from '~/types/execution'
|
|
10
|
+
import type { GateAttempt, GateStepState } from '~/types/execution'
|
|
11
11
|
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
12
12
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
13
13
|
|
|
14
14
|
const board = useBoardStore()
|
|
15
15
|
const execution = useExecutionStore()
|
|
16
|
+
const { t, d } = useI18n()
|
|
16
17
|
|
|
17
18
|
// Synchronous window: it reads its state straight off the execution step, so there's
|
|
18
19
|
// nothing to fetch on open (no `onOpen` loader).
|
|
@@ -39,10 +40,10 @@ const helperMeta = computed(() => agentKindMeta(helperKind.value))
|
|
|
39
40
|
|
|
40
41
|
const subtitle = computed(() =>
|
|
41
42
|
isHumanReview.value
|
|
42
|
-
? '
|
|
43
|
+
? t('gates.subtitle.humanReview')
|
|
43
44
|
: isCi.value
|
|
44
|
-
? '
|
|
45
|
-
: '
|
|
45
|
+
? t('gates.subtitle.ci')
|
|
46
|
+
: t('gates.subtitle.conflicts'),
|
|
46
47
|
)
|
|
47
48
|
|
|
48
49
|
// Human-review: approval progress + the freeform "request a fix" control.
|
|
@@ -67,8 +68,15 @@ const shortSha = computed(() => (gate.value?.headSha ? gate.value.headSha.slice(
|
|
|
67
68
|
// The helper-agent attempts this gate dispatched, newest first for the timeline.
|
|
68
69
|
const attempts = computed(() => [...(gate.value?.attemptLog ?? [])].reverse())
|
|
69
70
|
|
|
71
|
+
// Exhaustive map of the attempt outcome enum → label (literal keys keep the typed-key
|
|
72
|
+
// drift guard live, vs a runtime-built `gates.outcome.${outcome}`).
|
|
73
|
+
const OUTCOME_LABELS = computed<Record<GateAttempt['outcome'], string>>(() => ({
|
|
74
|
+
completed: t('gates.outcome.completed'),
|
|
75
|
+
failed: t('gates.outcome.failed'),
|
|
76
|
+
}))
|
|
77
|
+
|
|
70
78
|
function formatClock(ms?: number | null): string | null {
|
|
71
|
-
return ms ? new Date(ms)
|
|
79
|
+
return ms ? d(new Date(ms), 'long') : null
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/**
|
|
@@ -94,45 +102,62 @@ const status = computed<GateDisplayStatus>(() => {
|
|
|
94
102
|
return 'checking'
|
|
95
103
|
})
|
|
96
104
|
|
|
97
|
-
const STATUS_META
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
const STATUS_META = computed<
|
|
106
|
+
Record<
|
|
107
|
+
GateDisplayStatus,
|
|
108
|
+
{
|
|
109
|
+
label: string
|
|
110
|
+
badge: 'success' | 'warning' | 'error' | 'neutral'
|
|
111
|
+
icon: string
|
|
112
|
+
text: string
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
>(() => ({
|
|
101
116
|
passed: {
|
|
102
|
-
label: '
|
|
117
|
+
label: t('gates.status.passed'),
|
|
103
118
|
badge: 'success',
|
|
104
119
|
icon: 'i-lucide-circle-check',
|
|
105
120
|
text: 'text-emerald-300',
|
|
106
121
|
},
|
|
107
|
-
'gave-up': {
|
|
108
|
-
|
|
122
|
+
'gave-up': {
|
|
123
|
+
label: t('gates.status.gaveUp'),
|
|
124
|
+
badge: 'error',
|
|
125
|
+
icon: 'i-lucide-circle-x',
|
|
126
|
+
text: 'text-rose-300',
|
|
127
|
+
},
|
|
128
|
+
fixing: {
|
|
129
|
+
label: t('gates.status.fixing'),
|
|
130
|
+
badge: 'warning',
|
|
131
|
+
icon: 'i-lucide-loader',
|
|
132
|
+
text: 'text-amber-300',
|
|
133
|
+
},
|
|
109
134
|
failing: {
|
|
110
|
-
label: '
|
|
135
|
+
label: t('gates.status.failing'),
|
|
111
136
|
badge: 'error',
|
|
112
137
|
icon: 'i-lucide-circle-x',
|
|
113
138
|
text: 'text-rose-300',
|
|
114
139
|
},
|
|
115
140
|
pending: {
|
|
116
|
-
label: '
|
|
141
|
+
label: t('gates.status.pending'),
|
|
117
142
|
badge: 'neutral',
|
|
118
143
|
icon: 'i-lucide-clock',
|
|
119
144
|
text: 'text-slate-300',
|
|
120
145
|
},
|
|
121
146
|
checking: {
|
|
122
|
-
label: '
|
|
147
|
+
label: t('gates.status.checking'),
|
|
123
148
|
badge: 'neutral',
|
|
124
149
|
icon: 'i-lucide-loader',
|
|
125
150
|
text: 'text-slate-300',
|
|
126
151
|
},
|
|
127
|
-
}
|
|
152
|
+
}))
|
|
128
153
|
|
|
129
154
|
// The conflicts gate has no structured detail (GitHub reports mergeability as a single
|
|
130
155
|
// verdict, no file list), so the window shows the verdict + a note rather than a list.
|
|
131
156
|
const conflictVerdict = computed(() => {
|
|
132
|
-
if (status.value === 'passed') return '
|
|
133
|
-
if (gate.value?.lastVerdict === 'pending') return '
|
|
134
|
-
if (gate.value?.lastVerdict === 'fail') return '
|
|
135
|
-
return '
|
|
157
|
+
if (status.value === 'passed') return t('gates.conflict.mergeable')
|
|
158
|
+
if (gate.value?.lastVerdict === 'pending') return t('gates.conflict.computing')
|
|
159
|
+
if (gate.value?.lastVerdict === 'fail') return t('gates.conflict.conflicts')
|
|
160
|
+
return t('gates.conflict.unknown')
|
|
136
161
|
})
|
|
137
162
|
</script>
|
|
138
163
|
|
|
@@ -183,10 +208,9 @@ const conflictVerdict = computed(() => {
|
|
|
183
208
|
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
184
209
|
>
|
|
185
210
|
<UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
|
|
186
|
-
<p class="text-sm">
|
|
211
|
+
<p class="text-sm">{{ t('gates.noActivity') }}</p>
|
|
187
212
|
<p class="max-w-sm text-[11px] text-slate-500">
|
|
188
|
-
|
|
189
|
-
the board.
|
|
213
|
+
{{ t('gates.noActivityHint') }}
|
|
190
214
|
</p>
|
|
191
215
|
</div>
|
|
192
216
|
|
|
@@ -201,9 +225,7 @@ const conflictVerdict = computed(() => {
|
|
|
201
225
|
class="mt-0.5 h-4 w-4 shrink-0 text-emerald-400"
|
|
202
226
|
/>
|
|
203
227
|
<p class="text-[13px] leading-relaxed text-emerald-200">
|
|
204
|
-
{{
|
|
205
|
-
step?.output || (isCi ? 'CI is green.' : 'The PR merges cleanly with its base.')
|
|
206
|
-
}}
|
|
228
|
+
{{ step?.output || (isCi ? t('gates.passedCi') : t('gates.passedConflicts')) }}
|
|
207
229
|
</p>
|
|
208
230
|
</div>
|
|
209
231
|
|
|
@@ -214,14 +236,20 @@ const conflictVerdict = computed(() => {
|
|
|
214
236
|
>
|
|
215
237
|
<UIcon name="i-lucide-users" class="h-4 w-4 shrink-0 text-violet-300" />
|
|
216
238
|
<span class="text-[13px] text-slate-200">
|
|
217
|
-
{{
|
|
218
|
-
|
|
239
|
+
{{
|
|
240
|
+
t(
|
|
241
|
+
'gates.humanReview.approvals',
|
|
242
|
+
{ approved: gate.lastApprovals ?? 0, required: requiredApprovals },
|
|
243
|
+
requiredApprovals,
|
|
244
|
+
)
|
|
219
245
|
}}
|
|
220
|
-
<template v-if="status === 'fixing'">
|
|
246
|
+
<template v-if="status === 'fixing'">
|
|
247
|
+
{{ t('gates.humanReview.suffixFixing') }}</template
|
|
248
|
+
>
|
|
221
249
|
<template v-else-if="status === 'failing'">
|
|
222
|
-
|
|
250
|
+
{{ t('gates.humanReview.suffixFailing') }}</template
|
|
223
251
|
>
|
|
224
|
-
<template v-else>
|
|
252
|
+
<template v-else> {{ t('gates.humanReview.suffixAwaiting') }}</template>
|
|
225
253
|
</span>
|
|
226
254
|
</div>
|
|
227
255
|
<p
|
|
@@ -237,7 +265,7 @@ const conflictVerdict = computed(() => {
|
|
|
237
265
|
rel="noopener"
|
|
238
266
|
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
|
|
239
267
|
>
|
|
240
|
-
|
|
268
|
+
{{ t('gates.humanReview.reviewPr') }}
|
|
241
269
|
<UIcon name="i-lucide-external-link" class="h-3 w-3" />
|
|
242
270
|
</a>
|
|
243
271
|
|
|
@@ -246,17 +274,16 @@ const conflictVerdict = computed(() => {
|
|
|
246
274
|
<h3
|
|
247
275
|
class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
|
248
276
|
>
|
|
249
|
-
|
|
277
|
+
{{ t('gates.humanReview.requestFixHeading') }}
|
|
250
278
|
</h3>
|
|
251
279
|
<p class="mb-2 text-[11px] leading-relaxed text-slate-500">
|
|
252
|
-
|
|
253
|
-
review comments, which it addresses automatically).
|
|
280
|
+
{{ t('gates.humanReview.requestFixDescription') }}
|
|
254
281
|
</p>
|
|
255
282
|
<textarea
|
|
256
283
|
v-model="fixInstructions"
|
|
257
284
|
rows="3"
|
|
258
285
|
:disabled="fixBusy"
|
|
259
|
-
placeholder="
|
|
286
|
+
:placeholder="t('gates.humanReview.requestFixPlaceholder')"
|
|
260
287
|
class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none"
|
|
261
288
|
/>
|
|
262
289
|
<div class="mt-2 flex justify-end">
|
|
@@ -268,7 +295,7 @@ const conflictVerdict = computed(() => {
|
|
|
268
295
|
:disabled="fixBusy || fixInstructions.trim().length === 0"
|
|
269
296
|
@click="submitFix"
|
|
270
297
|
>
|
|
271
|
-
|
|
298
|
+
{{ t('gates.humanReview.requestFix') }}
|
|
272
299
|
</UButton>
|
|
273
300
|
</div>
|
|
274
301
|
</section>
|
|
@@ -277,7 +304,7 @@ const conflictVerdict = computed(() => {
|
|
|
277
304
|
<!-- CI: failing checks -->
|
|
278
305
|
<template v-else-if="isCi">
|
|
279
306
|
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
280
|
-
|
|
307
|
+
{{ t('gates.ci.failingChecks') }}
|
|
281
308
|
</h3>
|
|
282
309
|
<ul v-if="failingChecks.length" class="space-y-1">
|
|
283
310
|
<li
|
|
@@ -292,7 +319,7 @@ const conflictVerdict = computed(() => {
|
|
|
292
319
|
target="_blank"
|
|
293
320
|
rel="noopener"
|
|
294
321
|
class="group min-w-0 flex-1 truncate text-[13px] text-sky-300 hover:text-sky-200 hover:underline"
|
|
295
|
-
:title="
|
|
322
|
+
:title="t('gates.ci.openOnGithub', { name: c.name })"
|
|
296
323
|
>
|
|
297
324
|
{{ c.name }}
|
|
298
325
|
<UIcon
|
|
@@ -304,19 +331,19 @@ const conflictVerdict = computed(() => {
|
|
|
304
331
|
c.name
|
|
305
332
|
}}</span>
|
|
306
333
|
<span class="shrink-0 text-[11px] uppercase text-rose-300">
|
|
307
|
-
{{ c.conclusion ?? '
|
|
334
|
+
{{ c.conclusion ?? t('gates.ci.conclusionFallback') }}
|
|
308
335
|
</span>
|
|
309
336
|
</li>
|
|
310
337
|
</ul>
|
|
311
338
|
<p v-else class="text-[13px] leading-relaxed text-slate-300">
|
|
312
|
-
{{ gate.lastFailureSummary || '
|
|
339
|
+
{{ gate.lastFailureSummary || t('gates.ci.failureFallback') }}
|
|
313
340
|
</p>
|
|
314
341
|
</template>
|
|
315
342
|
|
|
316
343
|
<!-- Conflicts: verdict + the resolver's account of what it left -->
|
|
317
344
|
<template v-else>
|
|
318
345
|
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
319
|
-
|
|
346
|
+
{{ t('gates.conflicts.mergeability') }}
|
|
320
347
|
</h3>
|
|
321
348
|
<div
|
|
322
349
|
class="flex items-center gap-2 rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
@@ -344,7 +371,7 @@ const conflictVerdict = computed(() => {
|
|
|
344
371
|
rel="noopener"
|
|
345
372
|
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
|
|
346
373
|
>
|
|
347
|
-
|
|
374
|
+
{{ t('gates.conflicts.viewPr') }}
|
|
348
375
|
<UIcon name="i-lucide-external-link" class="h-3 w-3" />
|
|
349
376
|
</a>
|
|
350
377
|
</template>
|
|
@@ -352,7 +379,7 @@ const conflictVerdict = computed(() => {
|
|
|
352
379
|
<!-- Attempt history (both gates): what each helper run did and how it ended. -->
|
|
353
380
|
<section v-if="attempts.length" class="mt-5">
|
|
354
381
|
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
355
|
-
{{ helperMeta.label }}
|
|
382
|
+
{{ t('gates.attemptsHeading', { helper: helperMeta.label }) }}
|
|
356
383
|
</h3>
|
|
357
384
|
<ol class="space-y-2">
|
|
358
385
|
<li
|
|
@@ -361,14 +388,14 @@ const conflictVerdict = computed(() => {
|
|
|
361
388
|
class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
|
|
362
389
|
>
|
|
363
390
|
<div class="flex items-center gap-2">
|
|
364
|
-
<span class="text-[12px] font-semibold text-slate-200"
|
|
365
|
-
|
|
366
|
-
>
|
|
391
|
+
<span class="text-[12px] font-semibold text-slate-200">{{
|
|
392
|
+
t('gates.attempt', { number: a.attempt })
|
|
393
|
+
}}</span>
|
|
367
394
|
<UBadge
|
|
368
395
|
:color="a.outcome === 'failed' ? 'error' : 'neutral'"
|
|
369
396
|
variant="subtle"
|
|
370
397
|
size="sm"
|
|
371
|
-
>{{ a.outcome }}</UBadge
|
|
398
|
+
>{{ OUTCOME_LABELS[a.outcome] }}</UBadge
|
|
372
399
|
>
|
|
373
400
|
<span v-if="formatClock(a.at)" class="ml-auto text-[11px] text-slate-500">{{
|
|
374
401
|
formatClock(a.at)
|
|
@@ -392,7 +419,7 @@ const conflictVerdict = computed(() => {
|
|
|
392
419
|
>
|
|
393
420
|
<div v-if="gate">
|
|
394
421
|
<h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
395
|
-
|
|
422
|
+
{{ t('gates.sidebar.state') }}
|
|
396
423
|
</h4>
|
|
397
424
|
<div class="flex items-center gap-2 text-[13px]">
|
|
398
425
|
<UIcon
|
|
@@ -412,21 +439,29 @@ const conflictVerdict = computed(() => {
|
|
|
412
439
|
<!-- The human-review gate's budget is effectively unbounded (it waits for a human
|
|
413
440
|
indefinitely), so render a plain round count rather than "0/9007199254740991". -->
|
|
414
441
|
<template v-if="isHumanReview">
|
|
415
|
-
{{ gate.attempts }
|
|
442
|
+
{{ t('gates.sidebar.fixRounds', { count: gate.attempts }, gate.attempts) }}
|
|
416
443
|
</template>
|
|
417
444
|
<template v-else>
|
|
418
|
-
{{
|
|
419
|
-
|
|
445
|
+
{{
|
|
446
|
+
t(
|
|
447
|
+
'gates.sidebar.attempts',
|
|
448
|
+
{ attempts: gate.attempts, max: gate.maxAttempts },
|
|
449
|
+
gate.maxAttempts,
|
|
450
|
+
)
|
|
420
451
|
}}
|
|
421
452
|
</template>
|
|
422
|
-
<template v-if="gate.phase === 'working'">
|
|
423
|
-
|
|
453
|
+
<template v-if="gate.phase === 'working'">
|
|
454
|
+
{{ t('gates.sidebar.suffixRunning') }}</template
|
|
455
|
+
>
|
|
456
|
+
<template v-else-if="gate.attempts === 0">
|
|
457
|
+
{{ t('gates.sidebar.suffixNotNeeded') }}</template
|
|
458
|
+
>
|
|
424
459
|
</p>
|
|
425
460
|
</div>
|
|
426
461
|
|
|
427
462
|
<div v-if="shortSha">
|
|
428
463
|
<h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
429
|
-
|
|
464
|
+
{{ t('gates.sidebar.gatedCommit') }}
|
|
430
465
|
</h4>
|
|
431
466
|
<p class="font-mono text-[12px] text-slate-300">{{ shortSha }}</p>
|
|
432
467
|
</div>
|
|
@@ -444,8 +479,7 @@ const conflictVerdict = computed(() => {
|
|
|
444
479
|
/>
|
|
445
480
|
|
|
446
481
|
<p class="mt-auto text-[10px] leading-relaxed text-slate-600">
|
|
447
|
-
|
|
448
|
-
{{ helperMeta.label }} when it fails — a green check advances with nothing spun up.
|
|
482
|
+
{{ t('gates.sidebar.footer', { helper: helperMeta.label }) }}
|
|
449
483
|
</p>
|
|
450
484
|
</aside>
|
|
451
485
|
</div>
|
|
@@ -4,6 +4,7 @@ import { useLocalStorage } from '@vueuse/core'
|
|
|
4
4
|
import type { AgentKind } from '~/types/domain'
|
|
5
5
|
import { AGENT_CATEGORIES, OBSERVABILITY_GATE_ARCHETYPE } from '~/utils/catalog'
|
|
6
6
|
|
|
7
|
+
const { t } = useI18n()
|
|
7
8
|
const agents = useAgentsStore()
|
|
8
9
|
const releaseHealth = useReleaseHealthStore()
|
|
9
10
|
defineEmits<{ (e: 'add', kind: AgentKind): void }>()
|
|
@@ -25,7 +26,8 @@ const groups = computed(() => {
|
|
|
25
26
|
agents: palette.value.filter((a) => a.category === cat.id),
|
|
26
27
|
}))
|
|
27
28
|
const custom = palette.value.filter((a) => !a.category)
|
|
28
|
-
if (custom.length)
|
|
29
|
+
if (custom.length)
|
|
30
|
+
ordered.push({ id: 'custom', label: t('palette.customAgents'), agents: custom })
|
|
29
31
|
return ordered.filter((g) => g.agents.length)
|
|
30
32
|
})
|
|
31
33
|
|
|
@@ -43,7 +45,7 @@ function toggle(id: string) {
|
|
|
43
45
|
|
|
44
46
|
<template>
|
|
45
47
|
<div class="space-y-2">
|
|
46
|
-
<p class="px-1 text-[11px] text-slate-500">
|
|
48
|
+
<p class="px-1 text-[11px] text-slate-500">{{ t('palette.hint') }}</p>
|
|
47
49
|
<div class="space-y-2">
|
|
48
50
|
<section v-for="g in groups" :key="g.id">
|
|
49
51
|
<button
|
|
@@ -7,7 +7,7 @@ import type { IterationCapChoice } from '~/types/execution'
|
|
|
7
7
|
// or stop and reset the task. Used by both the requirements-review window and the
|
|
8
8
|
// companion step detail (Spec Reviewer / Reviewer / Architect Companion), so the two
|
|
9
9
|
// gates present an identical choice rather than each rolling its own.
|
|
10
|
-
withDefaults(
|
|
10
|
+
const props = withDefaults(
|
|
11
11
|
defineProps<{
|
|
12
12
|
heading: string
|
|
13
13
|
detail: string
|
|
@@ -16,14 +16,17 @@ withDefaults(
|
|
|
16
16
|
proceedLabel?: string
|
|
17
17
|
stopLabel?: string
|
|
18
18
|
}>(),
|
|
19
|
-
{
|
|
20
|
-
loading: false,
|
|
21
|
-
extraRoundLabel: 'One more round',
|
|
22
|
-
proceedLabel: 'Proceed anyway',
|
|
23
|
-
stopLabel: 'Stop & reset task',
|
|
24
|
-
},
|
|
19
|
+
{ loading: false },
|
|
25
20
|
)
|
|
26
21
|
|
|
22
|
+
const { t } = useI18n()
|
|
23
|
+
|
|
24
|
+
// The three button labels default to the shared i18n copy, but a caller may override
|
|
25
|
+
// any of them with surface-specific wording.
|
|
26
|
+
const extraRound = computed(() => props.extraRoundLabel ?? t('pipeline.iterationCap.extraRound'))
|
|
27
|
+
const proceed = computed(() => props.proceedLabel ?? t('pipeline.iterationCap.proceed'))
|
|
28
|
+
const stop = computed(() => props.stopLabel ?? t('pipeline.iterationCap.stopReset'))
|
|
29
|
+
|
|
27
30
|
const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
|
|
28
31
|
</script>
|
|
29
32
|
|
|
@@ -43,7 +46,7 @@ const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
|
|
|
43
46
|
:loading="loading"
|
|
44
47
|
@click="emit('resolve', 'extra-round')"
|
|
45
48
|
>
|
|
46
|
-
{{
|
|
49
|
+
{{ extraRound }}
|
|
47
50
|
</UButton>
|
|
48
51
|
<UButton
|
|
49
52
|
color="warning"
|
|
@@ -53,7 +56,7 @@ const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
|
|
|
53
56
|
:loading="loading"
|
|
54
57
|
@click="emit('resolve', 'proceed')"
|
|
55
58
|
>
|
|
56
|
-
{{
|
|
59
|
+
{{ proceed }}
|
|
57
60
|
</UButton>
|
|
58
61
|
<UButton
|
|
59
62
|
color="error"
|
|
@@ -63,7 +66,7 @@ const emit = defineEmits<{ resolve: [choice: IterationCapChoice] }>()
|
|
|
63
66
|
:loading="loading"
|
|
64
67
|
@click="emit('resolve', 'stop-reset')"
|
|
65
68
|
>
|
|
66
|
-
{{
|
|
69
|
+
{{ stop }}
|
|
67
70
|
</UButton>
|
|
68
71
|
</div>
|
|
69
72
|
</div>
|