@cat-factory/app 0.49.1 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/layout/IntegrationsHub.vue +3 -66
- package/app/components/layout/SideBar.vue +39 -0
- package/app/components/settings/ExecutionBackendSelector.vue +171 -0
- package/app/components/settings/InfrastructureWindow.vue +36 -126
- package/app/components/settings/KubernetesEnvironmentForm.vue +383 -0
- package/app/components/settings/LocalContainerPoolSettings.vue +181 -0
- package/app/components/settings/ProviderConnectionTab.vue +67 -30
- package/app/composables/api/providerConnections.ts +15 -10
- package/app/pages/index.vue +0 -4
- package/app/stores/auth.ts +10 -1
- package/app/stores/ui.ts +14 -18
- package/i18n/locales/en.json +61 -23
- package/i18n/locales/es.json +64 -26
- package/i18n/locales/fr.json +64 -26
- package/i18n/locales/pl.json +64 -26
- package/i18n/locales/uk.json +64 -26
- package/package.json +2 -2
- package/app/components/settings/LocalModeSettingsPanel.vue +0 -195
|
@@ -20,7 +20,6 @@ const documents = useDocumentsStore()
|
|
|
20
20
|
const tasks = useTasksStore()
|
|
21
21
|
const tracker = useTrackerStore()
|
|
22
22
|
const releaseHealth = useReleaseHealthStore()
|
|
23
|
-
const providerConnections = useProviderConnectionsStore()
|
|
24
23
|
const userSecrets = useUserSecretsStore()
|
|
25
24
|
const apiKeys = useApiKeysStore()
|
|
26
25
|
const workspace = useWorkspaceStore()
|
|
@@ -50,7 +49,6 @@ watch(
|
|
|
50
49
|
if (isOpen) {
|
|
51
50
|
query.value = ''
|
|
52
51
|
void releaseHealth.ensureLoaded().catch(() => {})
|
|
53
|
-
void providerConnections.ensureLoaded().catch(() => {})
|
|
54
52
|
void userSecrets.load().catch(() => {})
|
|
55
53
|
// Drives the OpenRouter row's "Key connected" badge.
|
|
56
54
|
if (workspace.workspaceId) void apiKeys.load(workspace.workspaceId).catch(() => {})
|
|
@@ -258,70 +256,9 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
258
256
|
})
|
|
259
257
|
}
|
|
260
258
|
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
// collapse into ONE row opening the tabbed Infrastructure window. The row shows a combined
|
|
265
|
-
// per-concern summary ("Agents: connected · Envs: not connected"). Each concern still gates
|
|
266
|
-
// on its own availability probe (a tab whose backend is off simply doesn't render), so the
|
|
267
|
-
// row appears whenever EITHER is available. The ProviderConfigBanner handles the louder
|
|
268
|
-
// "missing mandatory fields" warning.
|
|
269
|
-
const infra: IntegrationItem[] = []
|
|
270
|
-
const agentsAvailable = providerConnections.isAvailable('runner-pool')
|
|
271
|
-
const envsAvailable = providerConnections.isAvailable('environment')
|
|
272
|
-
if (agentsAvailable || envsAvailable) {
|
|
273
|
-
const agentsConn = providerConnections.connectionFor('runner-pool')
|
|
274
|
-
const envsConn = providerConnections.connectionFor('environment')
|
|
275
|
-
// Combined summary across the available concerns only.
|
|
276
|
-
const stateWord = (conn: unknown) =>
|
|
277
|
-
conn
|
|
278
|
-
? t('layout.integrationsHub.status.connected')
|
|
279
|
-
: t('layout.integrationsHub.status.notConnected')
|
|
280
|
-
const parts: string[] = []
|
|
281
|
-
if (agentsAvailable)
|
|
282
|
-
parts.push(
|
|
283
|
-
t('layout.integrationsHub.items.infrastructure.agents', { state: stateWord(agentsConn) }),
|
|
284
|
-
)
|
|
285
|
-
if (envsAvailable)
|
|
286
|
-
parts.push(
|
|
287
|
-
t('layout.integrationsHub.items.infrastructure.envs', { state: stateWord(envsConn) }),
|
|
288
|
-
)
|
|
289
|
-
// Default the window to the agents tab when available (the common case), else envs.
|
|
290
|
-
const defaultKind = agentsAvailable ? 'runner-pool' : 'environment'
|
|
291
|
-
// A concern counts as satisfied when it's unavailable (nothing to configure) or connected.
|
|
292
|
-
// The row is "connected" (green) only when EVERY available concern is connected — a
|
|
293
|
-
// half-connected state shows the amber "attention" badge instead, so the green badge can't
|
|
294
|
-
// claim "done" while one concern is still unconfigured.
|
|
295
|
-
const agentsSatisfied = !agentsAvailable || !!agentsConn
|
|
296
|
-
const envsSatisfied = !envsAvailable || !!envsConn
|
|
297
|
-
const allConnected = agentsSatisfied && envsSatisfied
|
|
298
|
-
const someConnected = !!agentsConn || !!envsConn
|
|
299
|
-
infra.push({
|
|
300
|
-
key: 'infrastructure',
|
|
301
|
-
icon: 'i-lucide-server-cog',
|
|
302
|
-
label: t('layout.integrationsHub.items.infrastructure.label'),
|
|
303
|
-
description: t('layout.integrationsHub.items.infrastructure.description'),
|
|
304
|
-
status: parts.join(' · '),
|
|
305
|
-
connected: allConnected,
|
|
306
|
-
attention: someConnected && !allConnected,
|
|
307
|
-
attentionLabel: parts.join(' · '),
|
|
308
|
-
onClick: () => go(() => ui.openProviderConnection(defaultKind)),
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
// Local-mode-only: the warm-container pool + checkout reuse for the local runner. Shown
|
|
312
|
-
// only on the local-mode service (the controller 503s elsewhere, and `auth.localMode`
|
|
313
|
-
// is set from /auth/config).
|
|
314
|
-
if (auth.localMode?.enabled) {
|
|
315
|
-
infra.push({
|
|
316
|
-
key: 'local-mode',
|
|
317
|
-
icon: 'i-lucide-container',
|
|
318
|
-
label: t('layout.integrationsHub.items.localMode.label'),
|
|
319
|
-
description: t('layout.integrationsHub.items.localMode.description'),
|
|
320
|
-
onClick: () => go(ui.openLocalModeSettings),
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
|
-
if (infra.length)
|
|
324
|
-
out.push({ title: t('layout.integrationsHub.groups.infrastructure'), items: infra })
|
|
259
|
+
// NOTE: Infrastructure (agent-container execution + Tester environments + the local-mode
|
|
260
|
+
// warm pool/checkout) is no longer listed here — it moved to its OWN top-level navbar menu
|
|
261
|
+
// (SideBar → "Infrastructure" → the tabbed Infrastructure window). See `ui.openInfrastructure`.
|
|
325
262
|
|
|
326
263
|
// --- Personal (only you) — fallback when there is no UserMenu to host "My setup" -------
|
|
327
264
|
// Per-user connections normally live in the My-setup hub; with auth disabled they fold in
|
|
@@ -20,8 +20,20 @@ const slack = useSlackStore()
|
|
|
20
20
|
const library = useFragmentLibraryStore()
|
|
21
21
|
const workspace = useWorkspaceStore()
|
|
22
22
|
const accounts = useAccountsStore()
|
|
23
|
+
const auth = useAuthStore()
|
|
24
|
+
const providerConnections = useProviderConnectionsStore()
|
|
23
25
|
const ui = useUiStore()
|
|
24
26
|
|
|
27
|
+
// The Infrastructure menu (agent-container execution + test environments) is relevant when
|
|
28
|
+
// either provider integration is available OR we're running the local-mode facade (which
|
|
29
|
+
// always has host-Docker execution + the warm-pool/checkout settings to configure).
|
|
30
|
+
const showInfrastructure = computed(
|
|
31
|
+
() =>
|
|
32
|
+
auth.localMode?.enabled === true ||
|
|
33
|
+
providerConnections.isAvailable('runner-pool') ||
|
|
34
|
+
providerConnections.isAvailable('environment'),
|
|
35
|
+
)
|
|
36
|
+
|
|
25
37
|
// `isCompact` (< lg) is the breakpoint at which the navbar is an off-canvas drawer;
|
|
26
38
|
// above it the aside is static and the drawer flag is inert.
|
|
27
39
|
const { isCompact } = useViewport()
|
|
@@ -89,6 +101,7 @@ watch(
|
|
|
89
101
|
void github.probe()
|
|
90
102
|
void slack.probe()
|
|
91
103
|
void library.probe()
|
|
104
|
+
void providerConnections.ensureLoaded().catch(() => {})
|
|
92
105
|
},
|
|
93
106
|
{ immediate: true },
|
|
94
107
|
)
|
|
@@ -241,6 +254,32 @@ watch(
|
|
|
241
254
|
</div>
|
|
242
255
|
</section>
|
|
243
256
|
|
|
257
|
+
<template v-if="showInfrastructure">
|
|
258
|
+
<USeparator />
|
|
259
|
+
<section>
|
|
260
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
261
|
+
{{ t('nav.infrastructure') }}
|
|
262
|
+
</h2>
|
|
263
|
+
<div class="space-y-1.5">
|
|
264
|
+
<!-- Where agent containers run + Tester environments + (local mode) the warm
|
|
265
|
+
pool/checkout reuse. Its own top-level destination, no longer inside the
|
|
266
|
+
Integrations hub. -->
|
|
267
|
+
<UButton
|
|
268
|
+
block
|
|
269
|
+
color="primary"
|
|
270
|
+
variant="soft"
|
|
271
|
+
size="sm"
|
|
272
|
+
icon="i-lucide-server-cog"
|
|
273
|
+
class="justify-start"
|
|
274
|
+
data-testid="nav-infrastructure"
|
|
275
|
+
@click="ui.openInfrastructure()"
|
|
276
|
+
>
|
|
277
|
+
{{ t('nav.infrastructure') }}
|
|
278
|
+
</UButton>
|
|
279
|
+
</div>
|
|
280
|
+
</section>
|
|
281
|
+
</template>
|
|
282
|
+
|
|
244
283
|
<template v-if="library.available">
|
|
245
284
|
<USeparator />
|
|
246
285
|
<section>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A clear picker for WHERE work runs, replacing the old bare "delegate to runner pool"
|
|
3
|
+
// yes/no switch. It reads the deployment's available backends from the capability descriptor
|
|
4
|
+
// (`auth.infrastructure`) and presents the actual options for THIS deployment:
|
|
5
|
+
// - execution axis → where agent containers run (local Docker host / Cloudflare Containers /
|
|
6
|
+
// self-hosted runner pool);
|
|
7
|
+
// - testEnv axis → where the Tester's ephemeral environments run (in-container
|
|
8
|
+
// docker-compose / an environment provider).
|
|
9
|
+
//
|
|
10
|
+
// The control is WRITABLE only in local mode, where the choice is a real per-workspace toggle
|
|
11
|
+
// (the `delegateAgentsToRunnerPool` / `delegateTestEnvToProvider` settings — this selector is a
|
|
12
|
+
// nicer face over those existing booleans, no new persistence). On the Worker/Node facades the
|
|
13
|
+
// active backend is determined by the deployment + whether a pool is registered (the Worker
|
|
14
|
+
// routes to a registered pool automatically, else Cloudflare Containers), so there it renders a
|
|
15
|
+
// read-only "Active: …" line instead of a control that wouldn't actually switch anything.
|
|
16
|
+
import { computed, ref } from 'vue'
|
|
17
|
+
import type { ExecutionBackendKind, TestEnvBackendKind } from '@cat-factory/contracts'
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{ axis: 'execution' | 'testEnv' }>()
|
|
20
|
+
|
|
21
|
+
const { t } = useI18n()
|
|
22
|
+
const auth = useAuthStore()
|
|
23
|
+
const settings = useWorkspaceSettingsStore()
|
|
24
|
+
const providerConnections = useProviderConnectionsStore()
|
|
25
|
+
const toast = useToast()
|
|
26
|
+
|
|
27
|
+
type BackendKind = ExecutionBackendKind | TestEnvBackendKind
|
|
28
|
+
|
|
29
|
+
// Per-axis labels — static literal keys whose leaves mirror the contract enum values verbatim
|
|
30
|
+
// (so the typed-message-keys check stays live and a dynamic lookup is total).
|
|
31
|
+
const EXECUTION_LABELS: Record<ExecutionBackendKind, string> = {
|
|
32
|
+
'local-docker': 'settings.infrastructure.executionBackend.local-docker',
|
|
33
|
+
'cloudflare-containers': 'settings.infrastructure.executionBackend.cloudflare-containers',
|
|
34
|
+
'runner-pool': 'settings.infrastructure.executionBackend.runner-pool',
|
|
35
|
+
}
|
|
36
|
+
const TEST_ENV_LABELS: Record<TestEnvBackendKind, string> = {
|
|
37
|
+
'local-compose': 'settings.infrastructure.testEnvBackend.local-compose',
|
|
38
|
+
'environment-provider': 'settings.infrastructure.testEnvBackend.environment-provider',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function labelFor(kind: BackendKind): string {
|
|
42
|
+
const key =
|
|
43
|
+
props.axis === 'execution'
|
|
44
|
+
? EXECUTION_LABELS[kind as ExecutionBackendKind]
|
|
45
|
+
: TEST_ENV_LABELS[kind as TestEnvBackendKind]
|
|
46
|
+
return t(key)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Normalise to the union element type so array ops (`includes`/`find`) don't collapse to
|
|
50
|
+
// `never` across the execution/testEnv discriminated union.
|
|
51
|
+
const cap = computed<{ available: BackendKind[]; active: BackendKind } | null>(() => {
|
|
52
|
+
const c = auth.infrastructure?.[props.axis]
|
|
53
|
+
return c ? { available: c.available as BackendKind[], active: c.active as BackendKind } : null
|
|
54
|
+
})
|
|
55
|
+
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
56
|
+
|
|
57
|
+
// The backend reached by "delegating" away from the on-machine default, plus the provider
|
|
58
|
+
// connection that must exist for it to work.
|
|
59
|
+
const delegatedKind = computed<BackendKind>(() =>
|
|
60
|
+
props.axis === 'execution' ? 'runner-pool' : 'environment-provider',
|
|
61
|
+
)
|
|
62
|
+
const connectionKind = computed<'runner-pool' | 'environment'>(() =>
|
|
63
|
+
props.axis === 'execution' ? 'runner-pool' : 'environment',
|
|
64
|
+
)
|
|
65
|
+
const delegatedRegistered = computed(
|
|
66
|
+
() => !!providerConnections.connectionFor(connectionKind.value),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// The on-machine / built-in default (the available option that isn't the delegated one).
|
|
70
|
+
const localKind = computed<BackendKind>(
|
|
71
|
+
() =>
|
|
72
|
+
cap.value?.available.find((k) => k !== delegatedKind.value) ??
|
|
73
|
+
cap.value?.active ??
|
|
74
|
+
'local-docker',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// The delegation flag is a genuine per-workspace toggle ONLY in local mode; elsewhere the
|
|
78
|
+
// active backend is registration/deployment-driven, so the control is read-only there.
|
|
79
|
+
const writable = computed(() => isLocal.value && (cap.value?.available.length ?? 0) > 1)
|
|
80
|
+
|
|
81
|
+
const delegated = computed(() =>
|
|
82
|
+
props.axis === 'execution'
|
|
83
|
+
? settings.settings.delegateAgentsToRunnerPool
|
|
84
|
+
: settings.settings.delegateTestEnvToProvider,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// The effective active backend. In local mode it follows the delegation flag; otherwise the
|
|
88
|
+
// delegated backend is active when its provider is registered (the Worker auto-routes to a
|
|
89
|
+
// registered pool), else the deployment default from the descriptor.
|
|
90
|
+
const activeKind = computed<BackendKind>(() => {
|
|
91
|
+
if (!cap.value) return localKind.value
|
|
92
|
+
if (writable.value) return delegated.value ? delegatedKind.value : localKind.value
|
|
93
|
+
if (cap.value.available.includes(delegatedKind.value) && delegatedRegistered.value) {
|
|
94
|
+
return delegatedKind.value
|
|
95
|
+
}
|
|
96
|
+
return cap.value.active
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const saving = ref(false)
|
|
100
|
+
|
|
101
|
+
async function select(kind: BackendKind) {
|
|
102
|
+
if (kind === activeKind.value) return
|
|
103
|
+
const toRunnerPool = kind === delegatedKind.value
|
|
104
|
+
saving.value = true
|
|
105
|
+
try {
|
|
106
|
+
await settings.update(
|
|
107
|
+
props.axis === 'execution'
|
|
108
|
+
? { delegateAgentsToRunnerPool: toRunnerPool }
|
|
109
|
+
: { delegateTestEnvToProvider: toRunnerPool },
|
|
110
|
+
)
|
|
111
|
+
} catch (e) {
|
|
112
|
+
toast.add({
|
|
113
|
+
title: t('settings.infrastructure.updateFailed'),
|
|
114
|
+
description: e instanceof Error ? e.message : String(e),
|
|
115
|
+
icon: 'i-lucide-triangle-alert',
|
|
116
|
+
color: 'error',
|
|
117
|
+
})
|
|
118
|
+
} finally {
|
|
119
|
+
saving.value = false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const labelKey = computed(() =>
|
|
124
|
+
props.axis === 'execution'
|
|
125
|
+
? 'settings.infrastructure.executionBackend.label'
|
|
126
|
+
: 'settings.infrastructure.testEnvBackend.label',
|
|
127
|
+
)
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<template>
|
|
131
|
+
<section v-if="cap" class="space-y-2 rounded-lg border border-slate-700 bg-slate-900/40 p-3">
|
|
132
|
+
<h3 class="text-sm font-semibold text-slate-200">{{ t(labelKey) }}</h3>
|
|
133
|
+
|
|
134
|
+
<!-- Local mode: a real choice the user can flip (writes the delegation setting). -->
|
|
135
|
+
<div v-if="writable" class="space-y-1.5" :data-testid="`${axis}-backend-options`">
|
|
136
|
+
<label
|
|
137
|
+
v-for="kind in cap.available"
|
|
138
|
+
:key="kind"
|
|
139
|
+
class="flex items-start gap-2"
|
|
140
|
+
:class="
|
|
141
|
+
kind === delegatedKind && !delegatedRegistered
|
|
142
|
+
? 'cursor-not-allowed opacity-50'
|
|
143
|
+
: 'cursor-pointer'
|
|
144
|
+
"
|
|
145
|
+
>
|
|
146
|
+
<input
|
|
147
|
+
type="radio"
|
|
148
|
+
class="mt-1"
|
|
149
|
+
:value="kind"
|
|
150
|
+
:checked="kind === activeKind"
|
|
151
|
+
:disabled="saving || (kind === delegatedKind && !delegatedRegistered)"
|
|
152
|
+
@change="select(kind)"
|
|
153
|
+
/>
|
|
154
|
+
<span class="min-w-0">
|
|
155
|
+
<span class="text-sm text-slate-200">{{ labelFor(kind) }}</span>
|
|
156
|
+
<span
|
|
157
|
+
v-if="kind === delegatedKind && !delegatedRegistered"
|
|
158
|
+
class="block text-[11px] text-amber-300/80"
|
|
159
|
+
>
|
|
160
|
+
{{ t('settings.infrastructure.registerHint') }}
|
|
161
|
+
</span>
|
|
162
|
+
</span>
|
|
163
|
+
</label>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Worker/Node: the active backend is deployment/registration-driven, not a toggle. -->
|
|
167
|
+
<p v-else class="text-sm text-slate-300" :data-testid="`${axis}-backend-active`">
|
|
168
|
+
{{ t('settings.infrastructure.active', { backend: labelFor(activeKind) }) }}
|
|
169
|
+
</p>
|
|
170
|
+
</section>
|
|
171
|
+
</template>
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// The single tabbed "Infrastructure" window
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
2
|
+
// The single tabbed "Infrastructure" window — now a TOP-LEVEL navbar destination (no longer
|
|
3
|
+
// reached through the Integrations hub). Two topical tabs:
|
|
4
|
+
// - "Agent containers" — where repo-operating agent containers run. Shows the execution
|
|
5
|
+
// backend selector, the runner-pool connection (ProviderConnectionTab), and — in local
|
|
6
|
+
// mode — the warm-container-pool + checkout-reuse settings (the local agent-container
|
|
7
|
+
// runtime, folded in from the former LocalModeSettingsPanel).
|
|
8
|
+
// - "Test environments" — where the Tester's ephemeral environments run. Shows the test-env
|
|
9
|
+
// backend selector and the environment-provider connection.
|
|
10
|
+
// Local-specific affordances render inline, gated on `auth.localMode?.enabled`. A tab whose
|
|
11
|
+
// backend integration is disabled (503) simply doesn't render.
|
|
12
12
|
import { computed, ref, watch } from 'vue'
|
|
13
13
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
14
|
-
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
15
14
|
import ProviderConnectionTab from '~/components/settings/ProviderConnectionTab.vue'
|
|
15
|
+
import ExecutionBackendSelector from '~/components/settings/ExecutionBackendSelector.vue'
|
|
16
|
+
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
16
17
|
|
|
17
18
|
const { t } = useI18n()
|
|
18
19
|
const ui = useUiStore()
|
|
19
20
|
const store = useProviderConnectionsStore()
|
|
20
21
|
const auth = useAuthStore()
|
|
21
|
-
const settings = useWorkspaceSettingsStore()
|
|
22
|
-
const toast = useToast()
|
|
23
22
|
|
|
24
23
|
const open = computed({
|
|
25
24
|
get: () => ui.infrastructureOpen,
|
|
@@ -27,7 +26,8 @@ const open = computed({
|
|
|
27
26
|
if (!v) ui.closeProviderConnection()
|
|
28
27
|
},
|
|
29
28
|
})
|
|
30
|
-
|
|
29
|
+
|
|
30
|
+
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
31
31
|
|
|
32
32
|
// Each concern gates on its own availability probe; an unavailable tab isn't offered.
|
|
33
33
|
const agentsAvailable = computed(() => store.isAvailable('runner-pool'))
|
|
@@ -38,7 +38,7 @@ const tabs = computed(() => {
|
|
|
38
38
|
if (agentsAvailable.value)
|
|
39
39
|
out.push({
|
|
40
40
|
value: 'runner-pool',
|
|
41
|
-
label: t('settings.providerConnection.tabs.
|
|
41
|
+
label: t('settings.providerConnection.tabs.agentContainers'),
|
|
42
42
|
icon: 'i-lucide-server-cog',
|
|
43
43
|
slot: 'runner-pool',
|
|
44
44
|
})
|
|
@@ -54,8 +54,8 @@ const tabs = computed(() => {
|
|
|
54
54
|
|
|
55
55
|
const activeTab = ref<ProviderConnectionKind>(ui.infrastructureTab)
|
|
56
56
|
|
|
57
|
-
// Honour the deep-linked tab each time the window opens
|
|
58
|
-
//
|
|
57
|
+
// Honour the deep-linked tab each time the window opens, falling back to the first available
|
|
58
|
+
// tab if the requested one is off.
|
|
59
59
|
watch(
|
|
60
60
|
open,
|
|
61
61
|
(isOpen) => {
|
|
@@ -69,9 +69,7 @@ watch(
|
|
|
69
69
|
)
|
|
70
70
|
// When availability resolves after open, re-pin onto a valid tab — but keep honouring the
|
|
71
71
|
// deep-linked request. The two availability probes resolve independently, so `tabs` can pass
|
|
72
|
-
// through a transient single-tab list
|
|
73
|
-
// environment one). We must NOT let that transient list steal focus from a still-loading
|
|
74
|
-
// requested tab, so only fall back to the first tab once loading has fully settled.
|
|
72
|
+
// through a transient single-tab list; only fall back to the first tab once loading settled.
|
|
75
73
|
watch([tabs, () => store.loaded], () => {
|
|
76
74
|
const list = tabs.value
|
|
77
75
|
if (list.some((x) => x.value === activeTab.value)) return
|
|
@@ -82,38 +80,6 @@ watch([tabs, () => store.loaded], () => {
|
|
|
82
80
|
activeTab.value = list[0]!.value
|
|
83
81
|
}
|
|
84
82
|
})
|
|
85
|
-
|
|
86
|
-
// --- Local-mode infrastructure delegation (cross-cutting; shown only in local mode) ---
|
|
87
|
-
// In local mode this is where a developer chooses, per workspace, whether to run on this
|
|
88
|
-
// machine (host Docker for agents, in-container docker-compose for the Tester) or delegate
|
|
89
|
-
// to an external service. Each toggle is enabled only once its provider is registered.
|
|
90
|
-
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
91
|
-
const runnerPoolRegistered = computed(() => !!store.connectionFor('runner-pool'))
|
|
92
|
-
const envRegistered = computed(() => !!store.connectionFor('environment'))
|
|
93
|
-
const savingDelegation = ref(false)
|
|
94
|
-
|
|
95
|
-
async function setDelegation(patch: {
|
|
96
|
-
delegateAgentsToRunnerPool?: boolean
|
|
97
|
-
delegateTestEnvToProvider?: boolean
|
|
98
|
-
}) {
|
|
99
|
-
savingDelegation.value = true
|
|
100
|
-
try {
|
|
101
|
-
await settings.update(patch)
|
|
102
|
-
} catch (e) {
|
|
103
|
-
toast.add({
|
|
104
|
-
title: t('settings.providerConnection.delegation.updateFailed'),
|
|
105
|
-
description: e instanceof Error ? e.message : String(e),
|
|
106
|
-
icon: 'i-lucide-triangle-alert',
|
|
107
|
-
color: 'error',
|
|
108
|
-
})
|
|
109
|
-
} finally {
|
|
110
|
-
savingDelegation.value = false
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function selectTab(kind: ProviderConnectionKind) {
|
|
115
|
-
activeTab.value = kind
|
|
116
|
-
}
|
|
117
83
|
</script>
|
|
118
84
|
|
|
119
85
|
<template>
|
|
@@ -122,80 +88,8 @@ function selectTab(kind: ProviderConnectionKind) {
|
|
|
122
88
|
:title="t('settings.providerConnection.windowTitle')"
|
|
123
89
|
:ui="{ content: 'max-w-xl' }"
|
|
124
90
|
>
|
|
125
|
-
<template #title>
|
|
126
|
-
<IntegrationBackTitle :title="t('settings.providerConnection.windowTitle')" @back="back" />
|
|
127
|
-
</template>
|
|
128
91
|
<template #body>
|
|
129
92
|
<div class="space-y-4">
|
|
130
|
-
<!-- Local-mode delegation: the local-vs-external choice for BOTH container agents
|
|
131
|
-
AND the Tester's ephemeral environments, made once here at the top. -->
|
|
132
|
-
<section
|
|
133
|
-
v-if="isLocal"
|
|
134
|
-
class="space-y-3 rounded-lg border border-slate-700 bg-slate-900/40 p-3"
|
|
135
|
-
>
|
|
136
|
-
<div>
|
|
137
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
138
|
-
{{ t('settings.providerConnection.delegation.title') }}
|
|
139
|
-
</h3>
|
|
140
|
-
<p class="mt-1 text-[11px] text-slate-400">
|
|
141
|
-
{{ t('settings.providerConnection.delegation.intro') }}
|
|
142
|
-
</p>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<!-- Container agents → self-hosted runner pool -->
|
|
146
|
-
<div class="space-y-1">
|
|
147
|
-
<label class="flex items-center gap-2">
|
|
148
|
-
<USwitch
|
|
149
|
-
size="sm"
|
|
150
|
-
:model-value="settings.settings.delegateAgentsToRunnerPool"
|
|
151
|
-
:disabled="savingDelegation || !runnerPoolRegistered"
|
|
152
|
-
@update:model-value="(v) => setDelegation({ delegateAgentsToRunnerPool: v })"
|
|
153
|
-
/>
|
|
154
|
-
<span class="text-sm text-slate-200">
|
|
155
|
-
{{ t('settings.providerConnection.delegation.agentsToggle') }}
|
|
156
|
-
</span>
|
|
157
|
-
</label>
|
|
158
|
-
<p class="pl-9 text-[11px] text-slate-400">
|
|
159
|
-
{{ t('settings.providerConnection.delegation.agentsHint') }}
|
|
160
|
-
<template v-if="!runnerPoolRegistered">
|
|
161
|
-
<i18n-t
|
|
162
|
-
keypath="settings.providerConnection.delegation.registerPoolPrompt"
|
|
163
|
-
tag="span"
|
|
164
|
-
scope="global"
|
|
165
|
-
>
|
|
166
|
-
<template #link>
|
|
167
|
-
<button
|
|
168
|
-
type="button"
|
|
169
|
-
class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
|
|
170
|
-
@click="selectTab('runner-pool')"
|
|
171
|
-
>
|
|
172
|
-
{{ t('settings.providerConnection.delegation.registerPoolLink') }}
|
|
173
|
-
</button>
|
|
174
|
-
</template>
|
|
175
|
-
</i18n-t>
|
|
176
|
-
</template>
|
|
177
|
-
</p>
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
<!-- Tester environments → environment provider -->
|
|
181
|
-
<div class="space-y-1">
|
|
182
|
-
<label class="flex items-center gap-2">
|
|
183
|
-
<USwitch
|
|
184
|
-
size="sm"
|
|
185
|
-
:model-value="settings.settings.delegateTestEnvToProvider"
|
|
186
|
-
:disabled="savingDelegation || !envRegistered"
|
|
187
|
-
@update:model-value="(v) => setDelegation({ delegateTestEnvToProvider: v })"
|
|
188
|
-
/>
|
|
189
|
-
<span class="text-sm text-slate-200">
|
|
190
|
-
{{ t('settings.providerConnection.delegation.envToggle') }}
|
|
191
|
-
</span>
|
|
192
|
-
</label>
|
|
193
|
-
<p class="pl-9 text-[11px] text-slate-400">
|
|
194
|
-
{{ t('settings.providerConnection.delegation.envHint') }}
|
|
195
|
-
</p>
|
|
196
|
-
</div>
|
|
197
|
-
</section>
|
|
198
|
-
|
|
199
93
|
<UTabs
|
|
200
94
|
v-if="tabs.length"
|
|
201
95
|
v-model="activeTab"
|
|
@@ -205,10 +99,26 @@ function selectTab(kind: ProviderConnectionKind) {
|
|
|
205
99
|
data-testid="infrastructure-tabs"
|
|
206
100
|
>
|
|
207
101
|
<template #runner-pool>
|
|
208
|
-
<
|
|
102
|
+
<div class="space-y-4">
|
|
103
|
+
<!-- Where agent containers run (writable in local mode; read-only elsewhere). -->
|
|
104
|
+
<ExecutionBackendSelector axis="execution" />
|
|
105
|
+
<ProviderConnectionTab kind="runner-pool" />
|
|
106
|
+
<!-- Local mode: the warm-pool + checkout reuse ARE the host agent-container
|
|
107
|
+
runtime, so they live here rather than in a separate menu. -->
|
|
108
|
+
<section v-if="isLocal" class="border-t border-slate-800 pt-4">
|
|
109
|
+
<h3 class="mb-3 text-sm font-semibold text-slate-200">
|
|
110
|
+
{{ t('settings.localMode.title') }}
|
|
111
|
+
</h3>
|
|
112
|
+
<LocalContainerPoolSettings />
|
|
113
|
+
</section>
|
|
114
|
+
</div>
|
|
209
115
|
</template>
|
|
210
116
|
<template #environment>
|
|
211
|
-
<
|
|
117
|
+
<div class="space-y-4">
|
|
118
|
+
<!-- Where Tester environments run (writable in local mode; read-only elsewhere). -->
|
|
119
|
+
<ExecutionBackendSelector axis="testEnv" />
|
|
120
|
+
<ProviderConnectionTab kind="environment" />
|
|
121
|
+
</div>
|
|
212
122
|
</template>
|
|
213
123
|
</UTabs>
|
|
214
124
|
|