@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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// • invalid pipelines (unknown agent kind / bad shape) — DELETE a custom one, RESEED a built-in;
|
|
5
5
|
// • outdated built-ins (a newer catalog definition is available) — RESEED to adopt it.
|
|
6
6
|
// Detection is client-side (see usePipelineHealth); the actions hit the pipelines store.
|
|
7
|
+
const { t } = useI18n()
|
|
7
8
|
const ui = useUiStore()
|
|
8
9
|
const pipelines = usePipelinesStore()
|
|
9
10
|
const { invalid, outdated, hasIssues } = usePipelineHealth()
|
|
@@ -39,9 +40,10 @@ async function run(id: string, action: () => Promise<unknown>, failTitle: string
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const reseed = (id: string) =>
|
|
43
|
+
const reseed = (id: string) =>
|
|
44
|
+
run(id, () => pipelines.reseed(id), t('pipeline.health.toast.reseedFailed'))
|
|
43
45
|
const remove = (id: string) =>
|
|
44
|
-
run(id, () => pipelines.removePipeline(id), '
|
|
46
|
+
run(id, () => pipelines.removePipeline(id), t('pipeline.health.toast.deleteFailed'))
|
|
45
47
|
|
|
46
48
|
/** Reseed every reseedable pipeline (outdated built-ins + invalid built-ins) in one go. */
|
|
47
49
|
async function reseedAll() {
|
|
@@ -61,11 +63,11 @@ const reseedableCount = computed(
|
|
|
61
63
|
</script>
|
|
62
64
|
|
|
63
65
|
<template>
|
|
64
|
-
<UModal v-model:open="open" title="
|
|
66
|
+
<UModal v-model:open="open" :title="t('pipeline.health.title')" :ui="{ content: 'max-w-2xl' }">
|
|
65
67
|
<template #body>
|
|
66
68
|
<div v-if="!hasIssues" class="py-6 text-center text-sm text-slate-400">
|
|
67
69
|
<UIcon name="i-lucide-check-circle-2" class="mx-auto mb-2 h-8 w-8 text-emerald-400" />
|
|
68
|
-
|
|
70
|
+
{{ t('pipeline.health.allValid') }}
|
|
69
71
|
</div>
|
|
70
72
|
|
|
71
73
|
<div v-else class="space-y-5">
|
|
@@ -73,11 +75,12 @@ const reseedableCount = computed(
|
|
|
73
75
|
<section v-if="invalid.length" class="space-y-2">
|
|
74
76
|
<div class="flex items-center gap-2">
|
|
75
77
|
<UIcon name="i-lucide-triangle-alert" class="h-4 w-4 text-rose-400" />
|
|
76
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
78
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
79
|
+
{{ t('pipeline.health.invalidHeading') }}
|
|
80
|
+
</h3>
|
|
77
81
|
</div>
|
|
78
82
|
<p class="text-[11px] text-slate-500">
|
|
79
|
-
|
|
80
|
-
start. Delete a custom pipeline, or reseed a built-in to restore its catalog definition.
|
|
83
|
+
{{ t('pipeline.health.invalidDescription') }}
|
|
81
84
|
</p>
|
|
82
85
|
<ul class="space-y-2">
|
|
83
86
|
<li
|
|
@@ -92,7 +95,7 @@ const reseedableCount = computed(
|
|
|
92
95
|
{{ h.pipeline.name }}
|
|
93
96
|
</span>
|
|
94
97
|
<UBadge v-if="h.pipeline.builtin" color="neutral" variant="subtle" size="xs">
|
|
95
|
-
|
|
98
|
+
{{ t('pipeline.health.builtinBadge') }}
|
|
96
99
|
</UBadge>
|
|
97
100
|
</div>
|
|
98
101
|
<ul class="mt-1 space-y-0.5">
|
|
@@ -116,7 +119,7 @@ const reseedableCount = computed(
|
|
|
116
119
|
:disabled="anyBusy"
|
|
117
120
|
@click="reseed(h.pipeline.id)"
|
|
118
121
|
>
|
|
119
|
-
|
|
122
|
+
{{ t('pipeline.health.reseed') }}
|
|
120
123
|
</UButton>
|
|
121
124
|
<UButton
|
|
122
125
|
v-else
|
|
@@ -128,7 +131,7 @@ const reseedableCount = computed(
|
|
|
128
131
|
:disabled="anyBusy"
|
|
129
132
|
@click="remove(h.pipeline.id)"
|
|
130
133
|
>
|
|
131
|
-
|
|
134
|
+
{{ t('pipeline.health.delete') }}
|
|
132
135
|
</UButton>
|
|
133
136
|
</div>
|
|
134
137
|
</li>
|
|
@@ -139,11 +142,12 @@ const reseedableCount = computed(
|
|
|
139
142
|
<section v-if="outdated.length" class="space-y-2">
|
|
140
143
|
<div class="flex items-center gap-2">
|
|
141
144
|
<UIcon name="i-lucide-arrow-up-circle" class="h-4 w-4 text-amber-400" />
|
|
142
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
145
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
146
|
+
{{ t('pipeline.health.updatesHeading') }}
|
|
147
|
+
</h3>
|
|
143
148
|
</div>
|
|
144
149
|
<p class="text-[11px] text-slate-500">
|
|
145
|
-
|
|
146
|
-
and archive state are kept).
|
|
150
|
+
{{ t('pipeline.health.updatesDescription') }}
|
|
147
151
|
</p>
|
|
148
152
|
<ul class="space-y-2">
|
|
149
153
|
<li
|
|
@@ -166,7 +170,7 @@ const reseedableCount = computed(
|
|
|
166
170
|
:disabled="anyBusy"
|
|
167
171
|
@click="reseed(h.pipeline.id)"
|
|
168
172
|
>
|
|
169
|
-
|
|
173
|
+
{{ t('pipeline.health.reseed') }}
|
|
170
174
|
</UButton>
|
|
171
175
|
</li>
|
|
172
176
|
</ul>
|
|
@@ -184,7 +188,7 @@ const reseedableCount = computed(
|
|
|
184
188
|
:loading="anyBusy"
|
|
185
189
|
@click="reseedAll"
|
|
186
190
|
>
|
|
187
|
-
|
|
191
|
+
{{ t('pipeline.health.reseedAll', { count: reseedableCount }) }}
|
|
188
192
|
</UButton>
|
|
189
193
|
<span v-else />
|
|
190
194
|
<UButton
|
|
@@ -193,7 +197,7 @@ const reseedableCount = computed(
|
|
|
193
197
|
:disabled="anyBusy"
|
|
194
198
|
@click="ui.closePipelineHealth()"
|
|
195
199
|
>
|
|
196
|
-
{{ hasIssues ? '
|
|
200
|
+
{{ hasIssues ? t('pipeline.health.dismiss') : t('pipeline.health.done') }}
|
|
197
201
|
</UButton>
|
|
198
202
|
</div>
|
|
199
203
|
</template>
|
|
@@ -22,6 +22,7 @@ const models = useModelsStore()
|
|
|
22
22
|
const ui = useUiStore()
|
|
23
23
|
const execution = useExecutionStore()
|
|
24
24
|
const reviews = useReviewStage()
|
|
25
|
+
const { t } = useI18n()
|
|
25
26
|
|
|
26
27
|
// While an iterative reviewer gate (requirements-review / clarity-review) folds the
|
|
27
28
|
// answers / re-reviews in the background it needs NO human, so its parked approval is
|
|
@@ -30,11 +31,11 @@ function reviewStageLabel(agentKind: string | undefined): string | null {
|
|
|
30
31
|
if (!reviews.isBackground(agentKind, props.instance.blockId)) return null
|
|
31
32
|
const stage = reviews.stageForBlock(props.instance.blockId)
|
|
32
33
|
return stage === 'incorporating'
|
|
33
|
-
? '
|
|
34
|
+
? t('pipeline.progress.stage.incorporating')
|
|
34
35
|
: stage === 'reviewing'
|
|
35
|
-
? '
|
|
36
|
+
? t('pipeline.progress.stage.reviewing')
|
|
36
37
|
: stage === 'recommending'
|
|
37
|
-
? '
|
|
38
|
+
? t('pipeline.progress.stage.recommending')
|
|
38
39
|
: null
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -51,9 +52,11 @@ function followUpPending(step: PipelineStep): number {
|
|
|
51
52
|
}
|
|
52
53
|
function followUpLabel(step: PipelineStep): string {
|
|
53
54
|
const items = step.followUps?.items ?? []
|
|
54
|
-
if (items.length === 0) return '
|
|
55
|
+
if (items.length === 0) return t('pipeline.progress.followUp.watching')
|
|
55
56
|
const pending = followUpPending(step)
|
|
56
|
-
return pending > 0
|
|
57
|
+
return pending > 0
|
|
58
|
+
? t('pipeline.progress.followUp.toDecide', { count: pending })
|
|
59
|
+
: t('pipeline.progress.followUp.allDecided')
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
// --- restart from a step -----------------------------------------------------
|
|
@@ -79,21 +82,41 @@ async function restartFromHere(i: number) {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/** Visual language for an individual agent's runtime state. */
|
|
82
|
-
const STATE_META
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const STATE_META = computed<Record<AgentState, { label: string; color: string; icon: string }>>(
|
|
86
|
+
() => ({
|
|
87
|
+
pending: {
|
|
88
|
+
label: t('pipeline.progress.state.pending'),
|
|
89
|
+
color: '#64748b',
|
|
90
|
+
icon: 'i-lucide-circle-dashed',
|
|
91
|
+
},
|
|
92
|
+
working: {
|
|
93
|
+
label: t('pipeline.progress.state.working'),
|
|
94
|
+
color: '#6366f1',
|
|
95
|
+
icon: 'i-lucide-loader',
|
|
96
|
+
},
|
|
97
|
+
waiting_decision: {
|
|
98
|
+
label: t('pipeline.progress.state.waiting_decision'),
|
|
99
|
+
color: '#f59e0b',
|
|
100
|
+
icon: 'i-lucide-circle-help',
|
|
101
|
+
},
|
|
102
|
+
done: {
|
|
103
|
+
label: t('pipeline.progress.state.done'),
|
|
104
|
+
color: '#22c55e',
|
|
105
|
+
icon: 'i-lucide-circle-check',
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
88
109
|
|
|
89
110
|
/** Visual language for the pipeline instance as a whole. */
|
|
90
|
-
const STATUS_META
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
111
|
+
const STATUS_META = computed<Record<ExecutionInstance['status'], { label: string; chip: string }>>(
|
|
112
|
+
() => ({
|
|
113
|
+
running: { label: t('pipeline.progress.status.running'), chip: 'primary' },
|
|
114
|
+
blocked: { label: t('pipeline.progress.status.blocked'), chip: 'warning' },
|
|
115
|
+
paused: { label: t('pipeline.progress.status.paused'), chip: 'neutral' },
|
|
116
|
+
done: { label: t('pipeline.progress.status.done'), chip: 'success' },
|
|
117
|
+
failed: { label: t('pipeline.progress.status.failed'), chip: 'error' },
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
97
120
|
|
|
98
121
|
const steps = computed(() => props.instance.steps)
|
|
99
122
|
const total = computed(() => steps.value.length)
|
|
@@ -116,7 +139,7 @@ function liveWorking(state: AgentState) {
|
|
|
116
139
|
* failed reads as "Failed" with a red cross, not a frozen "Working" loader.
|
|
117
140
|
*/
|
|
118
141
|
function stepVisual(state: AgentState) {
|
|
119
|
-
return isFailedStep(state, runFailed.value) ? FAILED_STEP_META : STATE_META[state]
|
|
142
|
+
return isFailedStep(state, runFailed.value) ? FAILED_STEP_META : STATE_META.value[state]
|
|
120
143
|
}
|
|
121
144
|
|
|
122
145
|
/** A step counts as fully complete only once its state is `done`. */
|
|
@@ -135,7 +158,7 @@ const overallProgress = computed(() => {
|
|
|
135
158
|
})
|
|
136
159
|
const overallPct = computed(() => Math.round(overallProgress.value * 100))
|
|
137
160
|
|
|
138
|
-
const statusMeta = computed(() => STATUS_META[props.instance.status])
|
|
161
|
+
const statusMeta = computed(() => STATUS_META.value[props.instance.status])
|
|
139
162
|
|
|
140
163
|
/** The agent the pipeline is currently centred on (for the summary line). */
|
|
141
164
|
const currentAgent = computed(() => {
|
|
@@ -171,13 +194,19 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
171
194
|
<div class="flex flex-wrap items-center gap-3">
|
|
172
195
|
<UBadge :color="statusMeta.chip as any" variant="subtle">{{ statusMeta.label }}</UBadge>
|
|
173
196
|
<span class="text-sm text-slate-300">
|
|
174
|
-
<
|
|
175
|
-
|
|
197
|
+
<i18n-t keypath="pipeline.progress.agentsComplete" tag="span" scope="global">
|
|
198
|
+
<template #completed>
|
|
199
|
+
<span class="font-semibold text-white">{{ completedCount }}</span>
|
|
200
|
+
</template>
|
|
201
|
+
<template #total>{{ total }}</template>
|
|
202
|
+
</i18n-t>
|
|
176
203
|
</span>
|
|
177
204
|
<span v-if="currentAgent && instance.status === 'running'" class="text-xs text-slate-500">
|
|
178
|
-
· currently {
|
|
205
|
+
· {{ t('pipeline.progress.currently', { agent: currentAgent }) }}
|
|
179
206
|
</span>
|
|
180
|
-
<span class="ml-auto font-mono text-sm tabular-nums text-slate-200">{{
|
|
207
|
+
<span class="ml-auto font-mono text-sm tabular-nums text-slate-200">{{
|
|
208
|
+
t('pipeline.progress.percent', { value: overallPct })
|
|
209
|
+
}}</span>
|
|
181
210
|
</div>
|
|
182
211
|
<UProgress :model-value="overallPct" class="mt-3" />
|
|
183
212
|
|
|
@@ -233,7 +262,11 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
233
262
|
>
|
|
234
263
|
<div
|
|
235
264
|
class="group flex cursor-pointer items-center gap-2"
|
|
236
|
-
:title="
|
|
265
|
+
:title="
|
|
266
|
+
s.output
|
|
267
|
+
? t('pipeline.progress.viewDetailsOutput')
|
|
268
|
+
: t('pipeline.progress.viewDetails')
|
|
269
|
+
"
|
|
237
270
|
@click="openStep(i)"
|
|
238
271
|
>
|
|
239
272
|
<div
|
|
@@ -254,13 +287,13 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
254
287
|
<span
|
|
255
288
|
v-if="isCompanionKind(s.agentKind)"
|
|
256
289
|
class="shrink-0 rounded bg-slate-700/60 px-1 text-[9px] font-medium uppercase tracking-wide text-slate-300"
|
|
257
|
-
title="
|
|
290
|
+
:title="t('pipeline.progress.companionTooltip')"
|
|
258
291
|
>
|
|
259
|
-
|
|
292
|
+
{{ t('pipeline.progress.companion') }}
|
|
260
293
|
</span>
|
|
261
294
|
</div>
|
|
262
295
|
<div class="text-[10px] uppercase tracking-wide text-slate-500">
|
|
263
|
-
|
|
296
|
+
{{ t('pipeline.progress.stepOf', { current: i + 1, total }) }}
|
|
264
297
|
</div>
|
|
265
298
|
</div>
|
|
266
299
|
<span
|
|
@@ -281,7 +314,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
281
314
|
variant="ghost"
|
|
282
315
|
size="xs"
|
|
283
316
|
class="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
284
|
-
title="
|
|
317
|
+
:title="t('pipeline.progress.restartTooltip')"
|
|
285
318
|
@click.stop="restartArmed = i"
|
|
286
319
|
/>
|
|
287
320
|
<template v-else>
|
|
@@ -294,7 +327,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
294
327
|
class="shrink-0"
|
|
295
328
|
@click.stop="restartFromHere(i)"
|
|
296
329
|
>
|
|
297
|
-
|
|
330
|
+
{{ t('pipeline.progress.restartFromHere') }}
|
|
298
331
|
</UButton>
|
|
299
332
|
<UButton
|
|
300
333
|
color="neutral"
|
|
@@ -304,7 +337,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
304
337
|
:disabled="restarting === i"
|
|
305
338
|
@click.stop="restartArmed = null"
|
|
306
339
|
>
|
|
307
|
-
|
|
340
|
+
{{ t('common.cancel') }}
|
|
308
341
|
</UButton>
|
|
309
342
|
</template>
|
|
310
343
|
</template>
|
|
@@ -330,16 +363,21 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
330
363
|
class="mt-2 flex items-center gap-1.5 text-[11px] text-sky-300"
|
|
331
364
|
>
|
|
332
365
|
<UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 shrink-0 animate-spin" />
|
|
333
|
-
<span>
|
|
366
|
+
<span>{{ t('pipeline.progress.spinningUpContainer') }}</span>
|
|
334
367
|
</div>
|
|
335
368
|
|
|
336
369
|
<!-- live subtask counts from the agent's todo list -->
|
|
337
370
|
<div v-if="s.subtasks && s.subtasks.total > 0" class="mt-2">
|
|
338
371
|
<div class="flex items-center justify-between text-[10px] text-slate-400">
|
|
339
372
|
<span>
|
|
340
|
-
{{
|
|
373
|
+
{{
|
|
374
|
+
t('pipeline.progress.subtasks', {
|
|
375
|
+
completed: s.subtasks.completed,
|
|
376
|
+
total: s.subtasks.total,
|
|
377
|
+
})
|
|
378
|
+
}}
|
|
341
379
|
<span v-if="s.subtasks.inProgress > 0" class="text-indigo-300">
|
|
342
|
-
|
|
380
|
+
{{ t('pipeline.progress.subtasksInProgress', { count: s.subtasks.inProgress }) }}
|
|
343
381
|
</span>
|
|
344
382
|
</span>
|
|
345
383
|
</div>
|
|
@@ -399,7 +437,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
399
437
|
all step metadata) lives in the step-detail overlay opened by click. -->
|
|
400
438
|
<p v-if="s.output" class="mt-2 flex items-center gap-1 text-[11px] text-slate-500">
|
|
401
439
|
<UIcon name="i-lucide-book-open-text" class="h-3 w-3 shrink-0" />
|
|
402
|
-
|
|
440
|
+
{{ t('pipeline.progress.clickToRead') }}
|
|
403
441
|
</p>
|
|
404
442
|
|
|
405
443
|
<!-- Conditionally-run companion (today the Tester's fixer): a distinct
|
|
@@ -423,7 +461,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
423
461
|
</span>
|
|
424
462
|
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
|
|
425
463
|
{{ agentKindMeta(companionByStep[i]!.kind).label }}
|
|
426
|
-
<span class="text-slate-500">(
|
|
464
|
+
<span class="text-slate-500">{{ t('pipeline.progress.companionSuffix') }}</span>
|
|
427
465
|
</span>
|
|
428
466
|
<span
|
|
429
467
|
class="shrink-0 text-[11px] font-medium"
|
|
@@ -454,7 +492,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
454
492
|
</span>
|
|
455
493
|
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
|
|
456
494
|
{{ FOLLOW_UP_COMPANION_META.label }}
|
|
457
|
-
<span class="text-slate-500">(
|
|
495
|
+
<span class="text-slate-500">{{ t('pipeline.progress.companionSuffix') }}</span>
|
|
458
496
|
</span>
|
|
459
497
|
<span
|
|
460
498
|
class="shrink-0 text-[11px] font-medium"
|
|
@@ -483,7 +521,9 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
483
521
|
icon="i-lucide-shield-check"
|
|
484
522
|
@click="emit('openApproval', s.approval.id)"
|
|
485
523
|
>
|
|
486
|
-
|
|
524
|
+
{{
|
|
525
|
+
t('pipeline.progress.reviewApprove', { agent: agentKindMeta(s.agentKind).label })
|
|
526
|
+
}}
|
|
487
527
|
</UButton>
|
|
488
528
|
</div>
|
|
489
529
|
|
|
@@ -496,7 +536,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
496
536
|
icon="i-lucide-circle-help"
|
|
497
537
|
@click="emit('openDecision', s.decision.id)"
|
|
498
538
|
>
|
|
499
|
-
|
|
539
|
+
{{ t('pipeline.progress.resolve', { question: s.decision.question }) }}
|
|
500
540
|
</UButton>
|
|
501
541
|
</div>
|
|
502
542
|
<p
|
package/app/stores/auth.ts
CHANGED
|
@@ -31,6 +31,14 @@ export const useAuthStore = defineStore(
|
|
|
31
31
|
* setup banner). Null on every other facade.
|
|
32
32
|
*/
|
|
33
33
|
const localMode = ref<LocalModeConfig | null>(null)
|
|
34
|
+
/**
|
|
35
|
+
* Local mode only: the source-control provider the user last chose to sign in with
|
|
36
|
+
* (its PAT lives server-side in env — this is just the non-secret choice). Persisted, so
|
|
37
|
+
* on a later load we silently re-mint a session from that env PAT without showing the
|
|
38
|
+
* login screen. Set on an explicit sign-in, cleared on logout / 401 (so logout actually
|
|
39
|
+
* signs out — no auto re-login loop).
|
|
40
|
+
*/
|
|
41
|
+
const autoLoginProvider = ref<'github' | 'gitlab' | null>(null)
|
|
34
42
|
/** True once the initial auth handshake has settled. */
|
|
35
43
|
const ready = ref(false)
|
|
36
44
|
|
|
@@ -83,6 +91,25 @@ export const useAuthStore = defineStore(
|
|
|
83
91
|
}
|
|
84
92
|
if (!user.value) token.value = null
|
|
85
93
|
}
|
|
94
|
+
|
|
95
|
+
// Local mode: if no live session resolved but the user previously signed in with a
|
|
96
|
+
// configured env PAT, silently re-mint a session from it — so an expired/rotated token
|
|
97
|
+
// never forces the login screen again. The token itself stays server-side; we only
|
|
98
|
+
// remembered the provider choice. Guard on the provider STILL being configured (PAT could
|
|
99
|
+
// have been removed) and clear the choice on failure so we fall back to the login screen
|
|
100
|
+
// instead of looping.
|
|
101
|
+
if (
|
|
102
|
+
localMode.value?.enabled === true &&
|
|
103
|
+
user.value === null &&
|
|
104
|
+
autoLoginProvider.value &&
|
|
105
|
+
localMode.value.patLogin?.configured.includes(autoLoginProvider.value)
|
|
106
|
+
) {
|
|
107
|
+
try {
|
|
108
|
+
await patLogin({ provider: autoLoginProvider.value })
|
|
109
|
+
} catch {
|
|
110
|
+
autoLoginProvider.value = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
86
113
|
// An already-signed-in user who followed an invite link redeems it here (a
|
|
87
114
|
// brand-new user redeems it server-side during signup/OAuth instead).
|
|
88
115
|
if (user.value) await maybeAcceptInvite()
|
|
@@ -153,6 +180,8 @@ export const useAuthStore = defineStore(
|
|
|
153
180
|
*/
|
|
154
181
|
async function patLogin(body: { provider: 'github' | 'gitlab'; token?: string }) {
|
|
155
182
|
applySession(await api.patLogin(body))
|
|
183
|
+
// Remember the choice so a later load re-mints the session from the env PAT silently.
|
|
184
|
+
autoLoginProvider.value = body.provider
|
|
156
185
|
}
|
|
157
186
|
|
|
158
187
|
/** Request a password-reset link by email (always resolves; never reveals existence). */
|
|
@@ -170,9 +199,18 @@ export const useAuthStore = defineStore(
|
|
|
170
199
|
api.logout().catch(() => {})
|
|
171
200
|
token.value = null
|
|
172
201
|
user.value = null
|
|
202
|
+
// Forget the remembered provider so logout sticks (otherwise bootstrap would
|
|
203
|
+
// immediately re-mint a session from the env PAT).
|
|
204
|
+
autoLoginProvider.value = null
|
|
173
205
|
}
|
|
174
206
|
|
|
175
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Called by the API client when a request comes back 401. Drops the dead session but KEEPS
|
|
209
|
+
* the remembered provider (unlike logout): a 401 from an expired/rotated token or a
|
|
210
|
+
* transient blip should let the next load silently re-mint from the env PAT, not force the
|
|
211
|
+
* login screen. The guarded re-mint in `bootstrap` clears the choice itself if it genuinely
|
|
212
|
+
* fails (PAT removed/revoked), so there's no re-login loop.
|
|
213
|
+
*/
|
|
176
214
|
function handleUnauthorized() {
|
|
177
215
|
token.value = null
|
|
178
216
|
user.value = null
|
|
@@ -184,6 +222,7 @@ export const useAuthStore = defineStore(
|
|
|
184
222
|
required,
|
|
185
223
|
providers,
|
|
186
224
|
localMode,
|
|
225
|
+
autoLoginProvider,
|
|
187
226
|
ready,
|
|
188
227
|
isAuthenticated,
|
|
189
228
|
needsLogin,
|
|
@@ -199,5 +238,5 @@ export const useAuthStore = defineStore(
|
|
|
199
238
|
handleUnauthorized,
|
|
200
239
|
}
|
|
201
240
|
},
|
|
202
|
-
{ persist: { pick: ['token'] } },
|
|
241
|
+
{ persist: { pick: ['token', 'autoLoginProvider'] } },
|
|
203
242
|
)
|