@cat-factory/app 0.182.1 → 0.184.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 +9 -0
- package/app/components/layout/IntegrationsHub.vue +6 -19
- package/app/components/pipeline/AgentPromptEditor.logic.spec.ts +93 -0
- package/app/components/pipeline/AgentPromptEditor.logic.ts +60 -0
- package/app/components/pipeline/AgentPromptEditor.vue +298 -0
- package/app/components/pipeline/PipelineBuilder.vue +62 -0
- package/app/components/sandbox/SandboxPanel.vue +63 -5
- package/app/components/settings/InfrastructureWindow.logic.spec.ts +100 -0
- package/app/components/settings/InfrastructureWindow.logic.ts +78 -0
- package/app/components/settings/InfrastructureWindow.vue +65 -46
- package/app/components/settings/PackageRegistriesPanel.vue +131 -123
- package/app/composables/api/agentPrompts.ts +41 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/usePipelineErrorToast.ts +8 -0
- package/app/pages/index.vue +0 -1
- package/app/stores/agentPrompts.ts +100 -0
- package/app/stores/packageRegistries.spec.ts +111 -0
- package/app/stores/packageRegistries.ts +21 -8
- package/app/stores/sandbox.ts +15 -0
- package/app/stores/ui/modals.ts +20 -21
- package/app/types/agent-prompts.ts +13 -0
- package/app/types/providerConnections.ts +12 -0
- package/i18n/locales/de.json +53 -11
- package/i18n/locales/en.json +68 -11
- package/i18n/locales/es.json +53 -11
- package/i18n/locales/fr.json +53 -11
- package/i18n/locales/he.json +53 -11
- package/i18n/locales/it.json +53 -11
- package/i18n/locales/ja.json +53 -11
- package/i18n/locales/pl.json +53 -11
- package/i18n/locales/tr.json +53 -11
- package/i18n/locales/uk.json +53 -11
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
SandboxExperimentStatus,
|
|
11
11
|
SandboxFixtureKind,
|
|
12
12
|
SandboxGrade,
|
|
13
|
+
SandboxPromptOrigin,
|
|
13
14
|
SandboxPromptVersion,
|
|
14
15
|
SandboxRun,
|
|
15
16
|
} from '~/types/sandbox'
|
|
@@ -35,6 +36,47 @@ const FIXTURE_KIND_LABEL = computed<Record<SandboxFixtureKind, string>>(() => ({
|
|
|
35
36
|
'repo-feature': t('sandbox.fixtureKind.repo-feature'),
|
|
36
37
|
'repo-bug': t('sandbox.fixtureKind.repo-bug'),
|
|
37
38
|
}))
|
|
39
|
+
/**
|
|
40
|
+
* Badge colour per prompt origin. An exhaustive Record over the closed union rather than a
|
|
41
|
+
* ternary, so adding an origin fails the typecheck here instead of silently rendering as
|
|
42
|
+
* "candidate" — the drift guard the i18n conventions ask for on enum-keyed lookups.
|
|
43
|
+
*/
|
|
44
|
+
const PROMPT_ORIGIN_COLOR: Record<SandboxPromptOrigin, 'neutral' | 'primary' | 'warning'> = {
|
|
45
|
+
baseline: 'neutral',
|
|
46
|
+
candidate: 'primary',
|
|
47
|
+
workspace: 'warning',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Which version is mid-promotion, so only its button spins. */
|
|
51
|
+
const promoting = ref<string | null>(null)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Promotion is offered on anything that is not already what the workspace runs: a graded
|
|
55
|
+
* candidate (the point of the tool) and an older workspace revision (rolling back). Not on the
|
|
56
|
+
* live row, where it is a no-op the backend would swallow anyway — and not on a shipped baseline,
|
|
57
|
+
* since "run what the product ships" is the revert in the prompt editor, not a promotion that
|
|
58
|
+
* would pin today's wording as a stored override.
|
|
59
|
+
*/
|
|
60
|
+
function canPromote(version: SandboxPromptVersion): boolean {
|
|
61
|
+
return version.origin !== 'baseline' && version.live !== true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function promote(version: SandboxPromptVersion) {
|
|
65
|
+
promoting.value = version.id
|
|
66
|
+
try {
|
|
67
|
+
await store.promotePrompt(version)
|
|
68
|
+
toast.add({
|
|
69
|
+
title: t('sandbox.prompts.promoted', { agent: version.agentKind }),
|
|
70
|
+
color: 'success',
|
|
71
|
+
icon: 'i-lucide-rocket',
|
|
72
|
+
})
|
|
73
|
+
} catch {
|
|
74
|
+
toast.add({ title: t('sandbox.prompts.promoteFailed'), color: 'error' })
|
|
75
|
+
} finally {
|
|
76
|
+
promoting.value = null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
const FIXTURE_ORIGIN_LABEL = computed<Record<'builtin' | 'custom', string>>(() => ({
|
|
39
81
|
builtin: t('sandbox.fixtureOrigin.builtin'),
|
|
40
82
|
custom: t('sandbox.fixtureOrigin.custom'),
|
|
@@ -508,17 +550,20 @@ async function archive(prompt: SandboxPromptVersion) {
|
|
|
508
550
|
<div class="min-w-0">
|
|
509
551
|
<div class="flex items-center gap-2">
|
|
510
552
|
<span class="truncate text-slate-200">{{ p.name }}</span>
|
|
511
|
-
<UBadge
|
|
512
|
-
:color="p.origin === 'baseline' ? 'neutral' : 'primary'"
|
|
513
|
-
variant="soft"
|
|
514
|
-
size="xs"
|
|
515
|
-
>
|
|
553
|
+
<UBadge :color="PROMPT_ORIGIN_COLOR[p.origin]" variant="soft" size="xs">
|
|
516
554
|
{{
|
|
517
555
|
p.origin === 'baseline'
|
|
518
556
|
? t('sandbox.baseline')
|
|
519
557
|
: t('sandbox.versionLabel', { version: p.version })
|
|
520
558
|
}}
|
|
521
559
|
</UBadge>
|
|
560
|
+
<UBadge v-if="p.origin === 'workspace'" color="warning" variant="soft" size="xs">
|
|
561
|
+
{{
|
|
562
|
+
p.live
|
|
563
|
+
? t('sandbox.prompts.liveInWorkspace')
|
|
564
|
+
: t('sandbox.prompts.fromWorkspace')
|
|
565
|
+
}}
|
|
566
|
+
</UBadge>
|
|
522
567
|
</div>
|
|
523
568
|
<span class="text-[11px] text-slate-500">{{ p.agentKind }}</span>
|
|
524
569
|
</div>
|
|
@@ -535,6 +580,19 @@ async function archive(prompt: SandboxPromptVersion) {
|
|
|
535
580
|
"
|
|
536
581
|
@click="edit(p)"
|
|
537
582
|
/>
|
|
583
|
+
<!-- Deploy: make this version the workspace's live prompt for its agent kind.
|
|
584
|
+
Offered on a graded candidate and on an older workspace revision (rolling
|
|
585
|
+
back), but not on the one already live, where it would be a no-op. -->
|
|
586
|
+
<UButton
|
|
587
|
+
v-if="canPromote(p)"
|
|
588
|
+
icon="i-lucide-rocket"
|
|
589
|
+
color="primary"
|
|
590
|
+
variant="ghost"
|
|
591
|
+
size="xs"
|
|
592
|
+
:loading="promoting === p.id"
|
|
593
|
+
:title="t('sandbox.prompts.promoteTitle')"
|
|
594
|
+
@click="promote(p)"
|
|
595
|
+
/>
|
|
538
596
|
<UButton
|
|
539
597
|
v-if="p.origin === 'candidate'"
|
|
540
598
|
icon="i-lucide-archive"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
infrastructureTabs,
|
|
4
|
+
openInfrastructureTab,
|
|
5
|
+
repinInfrastructureTab,
|
|
6
|
+
} from './InfrastructureWindow.logic'
|
|
7
|
+
|
|
8
|
+
const NONE = { agents: false, environments: false, packageRegistries: false }
|
|
9
|
+
|
|
10
|
+
describe('infrastructureTabs', () => {
|
|
11
|
+
it('shows nothing when no probe reports a backend', () => {
|
|
12
|
+
expect(infrastructureTabs(NONE)).toEqual([])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('rides the environment probe for shared stacks', () => {
|
|
16
|
+
// A shared stack is infra a Tester environment ATTACHES to, so it cannot stand on its own:
|
|
17
|
+
// without an environment backend there is nothing to attach it to.
|
|
18
|
+
expect(infrastructureTabs({ ...NONE, environments: true })).toEqual([
|
|
19
|
+
'environment',
|
|
20
|
+
'shared-stacks',
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('gates the registries tab on its own module probe', () => {
|
|
25
|
+
// The backend 503s the module with no encryption key. That is independent of any execution
|
|
26
|
+
// or test-env backend, so the tab can be the ONLY one a deployment shows.
|
|
27
|
+
expect(infrastructureTabs({ ...NONE, packageRegistries: true })).toEqual(['package-registries'])
|
|
28
|
+
expect(infrastructureTabs({ ...NONE, agents: true })).toEqual(['runner-pool'])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('orders tabs by the question they answer, not by which probe resolved', () => {
|
|
32
|
+
expect(
|
|
33
|
+
infrastructureTabs({ agents: true, environments: true, packageRegistries: true }),
|
|
34
|
+
).toEqual(['runner-pool', 'environment', 'shared-stacks', 'package-registries'])
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('openInfrastructureTab', () => {
|
|
39
|
+
it('honours an available deep-linked tab over the first one', () => {
|
|
40
|
+
expect(openInfrastructureTab(['runner-pool', 'package-registries'], 'package-registries')).toBe(
|
|
41
|
+
'package-registries',
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('falls back to the first available tab when the request is off', () => {
|
|
46
|
+
expect(openInfrastructureTab(['runner-pool'], 'package-registries')).toBe('runner-pool')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('keeps naming the requested tab when nothing is available yet', () => {
|
|
50
|
+
// Every probe is still in flight on the first open. Holding the request (rather than
|
|
51
|
+
// inventing a selection) is what lets the re-pin land on it the moment its probe resolves.
|
|
52
|
+
expect(openInfrastructureTab([], 'package-registries')).toBe('package-registries')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('repinInfrastructureTab', () => {
|
|
57
|
+
it('leaves the user where they are when their tab is still available', () => {
|
|
58
|
+
// The list GROWS as probes resolve; someone reading a tab must not be yanked off it.
|
|
59
|
+
expect(
|
|
60
|
+
repinInfrastructureTab(
|
|
61
|
+
['runner-pool', 'environment', 'package-registries'],
|
|
62
|
+
'environment',
|
|
63
|
+
'runner-pool',
|
|
64
|
+
true,
|
|
65
|
+
),
|
|
66
|
+
).toBe('environment')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('lands on the deep-linked tab once its probe resolves', () => {
|
|
70
|
+
// The registries probe is the slow one: the window opened on `runner-pool` because
|
|
71
|
+
// `package-registries` was not there yet, and this is the moment it arrives.
|
|
72
|
+
expect(
|
|
73
|
+
repinInfrastructureTab(
|
|
74
|
+
['runner-pool', 'package-registries'],
|
|
75
|
+
'shared-stacks',
|
|
76
|
+
'package-registries',
|
|
77
|
+
true,
|
|
78
|
+
),
|
|
79
|
+
).toBe('package-registries')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('does not steal the selection while the probes are still settling', () => {
|
|
83
|
+
// A transient list holding only the fastest probe's tab. Falling back to it here would pin
|
|
84
|
+
// the user to whichever probe won the race and then REJECT the deep link for being a peer
|
|
85
|
+
// of the current tab.
|
|
86
|
+
expect(
|
|
87
|
+
repinInfrastructureTab(['runner-pool'], 'package-registries', 'package-registries', false),
|
|
88
|
+
).toBe('package-registries')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('falls back to the first tab once loading settled and the request is unavailable', () => {
|
|
92
|
+
expect(
|
|
93
|
+
repinInfrastructureTab(['runner-pool'], 'package-registries', 'package-registries', true),
|
|
94
|
+
).toBe('runner-pool')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('holds the current tab when settling left nothing available at all', () => {
|
|
98
|
+
expect(repinInfrastructureTab([], 'environment', 'environment', true)).toBe('environment')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { InfrastructureTab } from '~/types/providerConnections'
|
|
2
|
+
|
|
3
|
+
// Pure tab arithmetic behind `InfrastructureWindow.vue`: which tabs exist for this deployment,
|
|
4
|
+
// and which one to show. Extracted so it is unit-tested directly rather than through the DOM —
|
|
5
|
+
// the window's tabs come from THREE probes that resolve independently (the infrastructure
|
|
6
|
+
// capability, the provider-connection store, the package-registries module), so the interesting
|
|
7
|
+
// behaviour is what happens while the list is still growing, which a rendered-component test
|
|
8
|
+
// reproduces only by accident.
|
|
9
|
+
//
|
|
10
|
+
// Labels stay in the component: a label resolved here would have to travel as a message KEY and
|
|
11
|
+
// be fed to `t()` as a variable, which is exactly what defeats the typed-message-key check. The
|
|
12
|
+
// component maps a tab onto a literal `t()` call through an exhaustive Record instead.
|
|
13
|
+
|
|
14
|
+
/** Which of the window's tabs this deployment can show, as their probes currently read. */
|
|
15
|
+
export interface InfrastructureTabAvailability {
|
|
16
|
+
/** An execution backend is reported (runner pool / container runtime / local). */
|
|
17
|
+
agents: boolean
|
|
18
|
+
/** A test-environment backend is reported. Also gates the shared-stacks tab. */
|
|
19
|
+
environments: boolean
|
|
20
|
+
/** The package-registries module answered its probe affirmatively (it 503s unconfigured). */
|
|
21
|
+
packageRegistries: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The window's tabs, in display order. Order is the reading order of the questions they answer:
|
|
26
|
+
* where agent containers run, where test environments run, what those environments attach to,
|
|
27
|
+
* and what a checkout may install from.
|
|
28
|
+
*
|
|
29
|
+
* Shared stacks ride the test-environment probe because a stack is infra an environment attaches
|
|
30
|
+
* to — there is nothing to attach without an environment backend.
|
|
31
|
+
*/
|
|
32
|
+
export function infrastructureTabs(available: InfrastructureTabAvailability): InfrastructureTab[] {
|
|
33
|
+
const tabs: InfrastructureTab[] = []
|
|
34
|
+
if (available.agents) tabs.push('runner-pool')
|
|
35
|
+
if (available.environments) tabs.push('environment', 'shared-stacks')
|
|
36
|
+
if (available.packageRegistries) tabs.push('package-registries')
|
|
37
|
+
return tabs
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The tab to show when the window OPENS. The deep-linked request wins whenever it is available;
|
|
42
|
+
* otherwise fall back to the first tab that is.
|
|
43
|
+
*
|
|
44
|
+
* Whatever tab was showing last time is deliberately NOT consulted: the caller has just asked
|
|
45
|
+
* for `requested`, and honouring a leftover selection over it would silently ignore the deep
|
|
46
|
+
* link. Falling back to `requested` when nothing is available at all keeps the ref naming the
|
|
47
|
+
* thing that was asked for, so the re-pin below can land on it once its probe resolves.
|
|
48
|
+
*/
|
|
49
|
+
export function openInfrastructureTab(
|
|
50
|
+
tabs: readonly InfrastructureTab[],
|
|
51
|
+
requested: InfrastructureTab,
|
|
52
|
+
): InfrastructureTab {
|
|
53
|
+
if (tabs.includes(requested)) return requested
|
|
54
|
+
return tabs[0] ?? requested
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The tab to show after the tab list CHANGES (a probe resolved). Unlike the open-time choice
|
|
59
|
+
* this one protects the user's current position: a tab list that grows under someone reading a
|
|
60
|
+
* tab must not yank them elsewhere.
|
|
61
|
+
*
|
|
62
|
+
* `settled` is what stops the transient single-tab list from stealing the selection. The probes
|
|
63
|
+
* resolve independently, so the list legitimately passes through states missing tabs that are
|
|
64
|
+
* about to appear; falling back to the first tab before the probes have settled would pin the
|
|
65
|
+
* user to whichever one happened to resolve first, and the deep-linked tab would then be
|
|
66
|
+
* REJECTED for being the current one's peer rather than shown.
|
|
67
|
+
*/
|
|
68
|
+
export function repinInfrastructureTab(
|
|
69
|
+
tabs: readonly InfrastructureTab[],
|
|
70
|
+
current: InfrastructureTab,
|
|
71
|
+
requested: InfrastructureTab,
|
|
72
|
+
settled: boolean,
|
|
73
|
+
): InfrastructureTab {
|
|
74
|
+
if (tabs.includes(current)) return current
|
|
75
|
+
if (tabs.includes(requested)) return requested
|
|
76
|
+
if (settled && tabs.length) return tabs[0]!
|
|
77
|
+
return current
|
|
78
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// The single tabbed "Infrastructure" window — now a TOP-LEVEL navbar destination (no longer
|
|
3
|
-
// reached through the Integrations hub).
|
|
3
|
+
// reached through the Integrations hub). Its topical tabs:
|
|
4
4
|
// - "Agent containers" — where repo-operating agent containers run. Shows the execution
|
|
5
5
|
// backend selector, the runner-pool connection (ProviderConnectionTab), and — in local
|
|
6
6
|
// mode — the warm-container-pool + checkout-reuse settings (the local agent-container
|
|
@@ -10,24 +10,33 @@
|
|
|
10
10
|
// Compose setup (formerly a standalone "Environment setup" sidebar entry — it writes a
|
|
11
11
|
// service's Compose recipe plus the workspace's Compose handler, so it belongs beside the
|
|
12
12
|
// settings it edits rather than at the same level as them).
|
|
13
|
+
// - "Shared stacks" — long-lived Compose infra a Tester environment attaches to, so it rides
|
|
14
|
+
// the same probe as the environments tab (nothing to attach to without one).
|
|
15
|
+
// - "Package registries" — the private npm registries a checkout installs from (formerly an
|
|
16
|
+
// Integrations-hub row). What a container can resolve its dependencies from is part of the
|
|
17
|
+
// execution environment, not an optional external system a workspace links in.
|
|
13
18
|
// Local-specific affordances render inline, gated on `auth.localMode?.enabled`. A tab whose
|
|
14
19
|
// backend integration is disabled (503) simply doesn't render.
|
|
15
20
|
import { computed, ref, watch } from 'vue'
|
|
16
|
-
import type {
|
|
21
|
+
import type { InfrastructureTab } from '~/types/providerConnections'
|
|
22
|
+
import {
|
|
23
|
+
infrastructureTabs,
|
|
24
|
+
openInfrastructureTab,
|
|
25
|
+
repinInfrastructureTab,
|
|
26
|
+
} from '~/components/settings/InfrastructureWindow.logic'
|
|
17
27
|
import InfrastructureBackendPicker from '~/components/settings/InfrastructureBackendPicker.vue'
|
|
18
28
|
import InfraHandlersConfigurator from '~/components/settings/InfraHandlersConfigurator.vue'
|
|
19
29
|
import DefaultProvisionTypeSection from '~/components/settings/DefaultProvisionTypeSection.vue'
|
|
20
30
|
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
21
31
|
import SharedStacksPanel from '~/components/settings/SharedStacksPanel.vue'
|
|
22
32
|
import ComposeEnvironmentSetupSection from '~/components/settings/ComposeEnvironmentSetupSection.vue'
|
|
23
|
-
|
|
24
|
-
// The shared-stacks tab uses its own slot key beyond the provider-connection kinds.
|
|
25
|
-
type InfraTabValue = ProviderConnectionKind | 'shared-stacks'
|
|
33
|
+
import PackageRegistriesPanel from '~/components/settings/PackageRegistriesPanel.vue'
|
|
26
34
|
|
|
27
35
|
const { t } = useI18n()
|
|
28
36
|
const ui = useUiStore()
|
|
29
37
|
const store = useProviderConnectionsStore()
|
|
30
38
|
const auth = useAuthStore()
|
|
39
|
+
const packageRegistries = usePackageRegistriesStore()
|
|
31
40
|
|
|
32
41
|
const open = computed({
|
|
33
42
|
get: () => ui.infrastructureOpen,
|
|
@@ -45,35 +54,41 @@ const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
|
45
54
|
const agentsAvailable = computed(() => (auth.infrastructure?.execution.available.length ?? 0) > 0)
|
|
46
55
|
const envsAvailable = computed(() => (auth.infrastructure?.testEnv.available.length ?? 0) > 0)
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Shared stacks are long-lived compose infra a Tester environment attaches to, so they live
|
|
65
|
-
// alongside the test-environment config (shown wherever an environment backend is available).
|
|
66
|
-
if (envsAvailable.value)
|
|
67
|
-
out.push({
|
|
68
|
-
value: 'shared-stacks',
|
|
69
|
-
label: t('settings.sharedStacks.tab'),
|
|
70
|
-
icon: 'i-lucide-layers',
|
|
71
|
-
slot: 'shared-stacks',
|
|
72
|
-
})
|
|
73
|
-
return out
|
|
74
|
-
})
|
|
57
|
+
// Tab presentation, keyed by the closed `InfrastructureTab` union so a new tab fails to compile
|
|
58
|
+
// until it is given a label and an icon. The labels are literal `t()` calls for the same reason
|
|
59
|
+
// they are not resolved in the logic module: a key assembled at runtime is invisible to the
|
|
60
|
+
// typed-message-key check.
|
|
61
|
+
const TAB_LABELS = computed<Record<InfrastructureTab, string>>(() => ({
|
|
62
|
+
'runner-pool': t('settings.providerConnection.tabs.agentContainers'),
|
|
63
|
+
environment: t('settings.providerConnection.tabs.testEnvironments'),
|
|
64
|
+
'shared-stacks': t('settings.sharedStacks.tab'),
|
|
65
|
+
'package-registries': t('settings.packageRegistries.tab'),
|
|
66
|
+
}))
|
|
67
|
+
const TAB_ICONS: Record<InfrastructureTab, string> = {
|
|
68
|
+
'runner-pool': 'i-lucide-server-cog',
|
|
69
|
+
environment: 'i-lucide-cloud',
|
|
70
|
+
'shared-stacks': 'i-lucide-layers',
|
|
71
|
+
'package-registries': 'i-lucide-package',
|
|
72
|
+
}
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
// `slot` mirrors `value` — the template names one `<template #…>` per tab value.
|
|
75
|
+
const tabs = computed(() =>
|
|
76
|
+
infrastructureTabs({
|
|
77
|
+
agents: agentsAvailable.value,
|
|
78
|
+
environments: envsAvailable.value,
|
|
79
|
+
// The module's own probe (the backend 503s with no encryption key), same gate as the
|
|
80
|
+
// Integrations-hub row this replaced — an unconfigured backend shows no dead tab.
|
|
81
|
+
packageRegistries: packageRegistries.available === true,
|
|
82
|
+
}).map((value) => ({
|
|
83
|
+
value,
|
|
84
|
+
label: TAB_LABELS.value[value],
|
|
85
|
+
icon: TAB_ICONS[value],
|
|
86
|
+
slot: value,
|
|
87
|
+
})),
|
|
88
|
+
)
|
|
89
|
+
const tabValues = computed(() => tabs.value.map((x) => x.value))
|
|
90
|
+
|
|
91
|
+
const activeTab = ref<InfrastructureTab>(ui.infrastructureTab)
|
|
77
92
|
|
|
78
93
|
// Honour the deep-linked tab each time the window opens, falling back to the first available
|
|
79
94
|
// tab if the requested one is off.
|
|
@@ -82,24 +97,25 @@ watch(
|
|
|
82
97
|
(isOpen) => {
|
|
83
98
|
if (!isOpen) return
|
|
84
99
|
void store.ensureLoaded().catch(() => {})
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
// The registries tab gates on this probe, so it has to resolve for the tab to appear at
|
|
101
|
+
// all — the panel's own load is a no-op once this settled (`ensureLoaded` coalesces).
|
|
102
|
+
// Swallowed here on purpose: the PANEL reports a load failure, and it can only do that
|
|
103
|
+
// once the tab it lives in exists, so a probe failure has to leave the window itself alone.
|
|
104
|
+
void packageRegistries.ensureLoaded().catch(() => {})
|
|
105
|
+
activeTab.value = openInfrastructureTab(tabValues.value, ui.infrastructureTab)
|
|
88
106
|
},
|
|
89
107
|
{ immediate: true },
|
|
90
108
|
)
|
|
91
109
|
// When availability resolves after open, re-pin onto a valid tab — but keep honouring the
|
|
92
|
-
// deep-linked request. The
|
|
93
|
-
// through a transient
|
|
110
|
+
// deep-linked request. The three availability probes resolve independently, so `tabs` can pass
|
|
111
|
+
// through a transient short list; only fall back to the first tab once loading settled.
|
|
94
112
|
watch([tabs, () => store.loaded], () => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
activeTab.value = list[0]!.value
|
|
102
|
-
}
|
|
113
|
+
activeTab.value = repinInfrastructureTab(
|
|
114
|
+
tabValues.value,
|
|
115
|
+
activeTab.value,
|
|
116
|
+
ui.infrastructureTab,
|
|
117
|
+
store.loaded,
|
|
118
|
+
)
|
|
103
119
|
})
|
|
104
120
|
</script>
|
|
105
121
|
|
|
@@ -157,6 +173,9 @@ watch([tabs, () => store.loaded], () => {
|
|
|
157
173
|
<template #shared-stacks>
|
|
158
174
|
<SharedStacksPanel />
|
|
159
175
|
</template>
|
|
176
|
+
<template #package-registries>
|
|
177
|
+
<PackageRegistriesPanel />
|
|
178
|
+
</template>
|
|
160
179
|
</UTabs>
|
|
161
180
|
|
|
162
181
|
<p v-else class="px-1 py-6 text-center text-sm text-slate-500">
|