@cat-factory/app 0.176.0 → 0.178.1
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 +28 -1
- package/app/components/documents/DocumentImportModal.vue +3 -17
- package/app/components/documents/DocumentTemplatesModal.vue +1 -1
- package/app/components/documents/SpawnPreviewModal.vue +9 -19
- package/app/components/environments/steps/EnvPickStep.vue +3 -0
- package/app/components/layout/CommandBar.vue +1 -1
- package/app/components/layout/IntegrationBackTitle.vue +11 -8
- package/app/components/layout/IntegrationsHub.vue +22 -56
- package/app/components/layout/ModelProvidersHub.vue +250 -0
- package/app/components/panels/InspectorPanel.vue +2 -19
- package/app/components/panels/inspector/ServiceValidationConfig.vue +99 -1
- package/app/components/settings/ComposeEnvironmentSetupSection.vue +102 -0
- package/app/components/settings/InfrastructureWindow.vue +11 -1
- package/app/composables/api/validationChecks.ts +8 -0
- package/app/composables/useIntegrationBack.ts +5 -2
- package/app/composables/useNavContributions.ts +1 -1
- package/app/modular/nav-contributions.spec.ts +11 -3
- package/app/modular/nav-contributions.ts +35 -16
- package/app/pages/index.vue +4 -0
- package/app/stores/documents.ts +9 -6
- package/app/stores/ui/modals.ts +43 -27
- package/app/stores/validationChecks.ts +17 -2
- package/app/types/validationChecks.ts +3 -0
- package/app/utils/validationDetection.spec.ts +51 -0
- package/app/utils/validationDetection.ts +67 -0
- package/i18n/locales/de.json +103 -27
- package/i18n/locales/en.json +103 -27
- package/i18n/locales/es.json +103 -27
- package/i18n/locales/fr.json +103 -27
- package/i18n/locales/he.json +103 -27
- package/i18n/locales/it.json +103 -27
- package/i18n/locales/ja.json +103 -27
- package/i18n/locales/pl.json +103 -27
- package/i18n/locales/tr.json +103 -27
- package/i18n/locales/uk.json +103 -27
- package/package.json +2 -2
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
VALIDATION_MAX_ATTEMPTS_CEILING,
|
|
8
8
|
VALIDATION_MAX_CHECKS,
|
|
9
9
|
type ValidationCheck,
|
|
10
|
+
type ValidationEcosystem,
|
|
10
11
|
} from '~/types/validationChecks'
|
|
12
|
+
import { mergeDetectedChecks } from '~/utils/validationDetection'
|
|
11
13
|
|
|
12
14
|
// Per-service (frame) PRE-PR VALIDATION CHECKS: the shell commands the executor-harness runs
|
|
13
15
|
// against the checkout after the coder settles and BEFORE the PR opens. A failing command's
|
|
@@ -18,10 +20,11 @@ const props = defineProps<{ block: Block }>()
|
|
|
18
20
|
|
|
19
21
|
const store = useValidationChecksStore()
|
|
20
22
|
const toast = useToast()
|
|
21
|
-
const { t } = useI18n()
|
|
23
|
+
const { t, te } = useI18n()
|
|
22
24
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
23
25
|
|
|
24
26
|
const busy = ref(false)
|
|
27
|
+
const detecting = ref(false)
|
|
25
28
|
const rows = ref<ValidationCheck[]>([])
|
|
26
29
|
const maxAttempts = ref(VALIDATION_DEFAULT_MAX_ATTEMPTS)
|
|
27
30
|
|
|
@@ -64,6 +67,88 @@ function notifyError(title: string, e: unknown) {
|
|
|
64
67
|
})
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
// Ecosystem label KEYS, exhaustive over the contracts `ValidationEcosystem` union: a new
|
|
71
|
+
// backend detector fails THIS typecheck until it is mapped (the key is assembled at runtime,
|
|
72
|
+
// so the typed-message-keys check cannot see the `t()` lookup — the map's exhaustiveness is
|
|
73
|
+
// the drift guard, same pattern as `ENV_TEST_STAGE_KEYS`).
|
|
74
|
+
const ECOSYSTEM_KEYS: Record<ValidationEcosystem, string> = {
|
|
75
|
+
node: 'inspector.validationChecks.ecosystem.node',
|
|
76
|
+
python: 'inspector.validationChecks.ecosystem.python',
|
|
77
|
+
go: 'inspector.validationChecks.ecosystem.go',
|
|
78
|
+
rust: 'inspector.validationChecks.ecosystem.rust',
|
|
79
|
+
maven: 'inspector.validationChecks.ecosystem.maven',
|
|
80
|
+
gradle: 'inspector.validationChecks.ecosystem.gradle',
|
|
81
|
+
dotnet: 'inspector.validationChecks.ecosystem.dotnet',
|
|
82
|
+
ruby: 'inspector.validationChecks.ecosystem.ruby',
|
|
83
|
+
php: 'inspector.validationChecks.ecosystem.php',
|
|
84
|
+
elixir: 'inspector.validationChecks.ecosystem.elixir',
|
|
85
|
+
make: 'inspector.validationChecks.ecosystem.make',
|
|
86
|
+
just: 'inspector.validationChecks.ecosystem.just',
|
|
87
|
+
task: 'inspector.validationChecks.ecosystem.task',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ecosystemLabel(id: ValidationEcosystem): string {
|
|
91
|
+
const key = ECOSYSTEM_KEYS[id]
|
|
92
|
+
// `te`-guarded so a locale missing the key shows the raw id, never a raw message key.
|
|
93
|
+
return te(key) ? t(key) : id
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fill the rows from what the service's repo declares. The suggestion is NOT saved — it
|
|
98
|
+
* lands in the same unsaved rows the operator edits by hand, so Detect is always reversible
|
|
99
|
+
* by walking away from the panel.
|
|
100
|
+
*/
|
|
101
|
+
async function detect() {
|
|
102
|
+
detecting.value = true
|
|
103
|
+
try {
|
|
104
|
+
const result = await store.detect(props.block.id)
|
|
105
|
+
if (result.status !== 'ok') {
|
|
106
|
+
// The backend distinguishes "no repo linked" from "the repo could not be read"; say
|
|
107
|
+
// which, because they send the operator to different places.
|
|
108
|
+
toast.add({
|
|
109
|
+
title: t(`inspector.validationChecks.detect.${result.status}`),
|
|
110
|
+
icon: 'i-lucide-triangle-alert',
|
|
111
|
+
color: 'warning',
|
|
112
|
+
})
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const merged = mergeDetectedChecks(rows.value, result.checks, VALIDATION_MAX_CHECKS)
|
|
116
|
+
rows.value = merged.rows
|
|
117
|
+
if (merged.added === 0) {
|
|
118
|
+
toast.add({
|
|
119
|
+
title: t('inspector.validationChecks.detect.nothingNew'),
|
|
120
|
+
description:
|
|
121
|
+
result.checks.length > 0
|
|
122
|
+
? t('inspector.validationChecks.detect.alreadyPresent')
|
|
123
|
+
: t('inspector.validationChecks.detect.unrecognised'),
|
|
124
|
+
icon: 'i-lucide-info',
|
|
125
|
+
color: 'neutral',
|
|
126
|
+
})
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const names = result.ecosystems.map(ecosystemLabel).join(', ')
|
|
130
|
+
toast.add({
|
|
131
|
+
title: t('inspector.validationChecks.detect.added', { count: merged.added }, merged.added),
|
|
132
|
+
// Name what was recognised AND what was left out: a cap that silently swallowed a
|
|
133
|
+
// suggestion reads as "that is everything your repo has".
|
|
134
|
+
description: [
|
|
135
|
+
names ? t('inspector.validationChecks.detect.found', { ecosystems: names }) : '',
|
|
136
|
+
merged.dropped > 0 || result.truncated
|
|
137
|
+
? t('inspector.validationChecks.detect.capped', { max: VALIDATION_MAX_CHECKS })
|
|
138
|
+
: '',
|
|
139
|
+
]
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join(' '),
|
|
142
|
+
icon: 'i-lucide-wand-sparkles',
|
|
143
|
+
color: 'success',
|
|
144
|
+
})
|
|
145
|
+
} catch (e) {
|
|
146
|
+
notifyError(t('inspector.validationChecks.detect.failed'), e)
|
|
147
|
+
} finally {
|
|
148
|
+
detecting.value = false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
67
152
|
async function save() {
|
|
68
153
|
busy.value = true
|
|
69
154
|
try {
|
|
@@ -179,6 +264,19 @@ async function clear() {
|
|
|
179
264
|
/>
|
|
180
265
|
</UFormField>
|
|
181
266
|
<div class="flex gap-2">
|
|
267
|
+
<UButton
|
|
268
|
+
color="neutral"
|
|
269
|
+
variant="soft"
|
|
270
|
+
size="xs"
|
|
271
|
+
icon="i-lucide-wand-sparkles"
|
|
272
|
+
:loading="detecting"
|
|
273
|
+
:disabled="!canAdd"
|
|
274
|
+
:title="t('inspector.validationChecks.detect.hint')"
|
|
275
|
+
data-testid="validation-detect"
|
|
276
|
+
@click="detect"
|
|
277
|
+
>
|
|
278
|
+
{{ t('inspector.validationChecks.detect.action') }}
|
|
279
|
+
</UButton>
|
|
182
280
|
<UButton
|
|
183
281
|
color="neutral"
|
|
184
282
|
variant="soft"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The guided per-service Docker Compose environment setup, as a section of the Infrastructure
|
|
3
|
+
// window's "Test environments" tab.
|
|
4
|
+
//
|
|
5
|
+
// It used to be its own top-level sidebar destination called "Environment setup", which was
|
|
6
|
+
// wrong twice over: the name said nothing about WHAT it sets up (people read it as the place
|
|
7
|
+
// to configure environments in general, which is the rest of this tab), and a wizard that
|
|
8
|
+
// fills in one field of one service's config sat at the same level as the workspace-wide
|
|
9
|
+
// infrastructure it depends on. It lives here now, next to the two settings it writes: the
|
|
10
|
+
// service's provision type and the workspace's Docker Compose handler.
|
|
11
|
+
//
|
|
12
|
+
// The section deliberately carries a full explanation rather than a one-line hint. The wizard
|
|
13
|
+
// is a five-minute flow that touches a repo and can trial-provision a stack, so "when you need
|
|
14
|
+
// this / when you don't" has to be answerable BEFORE opening it — most services on most boards
|
|
15
|
+
// never need it at all.
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
const ui = useUiStore()
|
|
18
|
+
|
|
19
|
+
/** Bullet lists rendered below; static literal keys so the typed-key check sees them. */
|
|
20
|
+
const steps = computed(() => [
|
|
21
|
+
t('settings.composeEnvSetup.how.scan'),
|
|
22
|
+
t('settings.composeEnvSetup.how.analyse'),
|
|
23
|
+
t('settings.composeEnvSetup.how.preflight'),
|
|
24
|
+
t('settings.composeEnvSetup.how.save'),
|
|
25
|
+
])
|
|
26
|
+
const needed = computed(() => [
|
|
27
|
+
t('settings.composeEnvSetup.needed.running'),
|
|
28
|
+
t('settings.composeEnvSetup.needed.compose'),
|
|
29
|
+
t('settings.composeEnvSetup.needed.setup'),
|
|
30
|
+
])
|
|
31
|
+
const notNeeded = computed(() => [
|
|
32
|
+
t('settings.composeEnvSetup.notNeeded.inContainer'),
|
|
33
|
+
t('settings.composeEnvSetup.notNeeded.otherBackend'),
|
|
34
|
+
t('settings.composeEnvSetup.notNeeded.noRepo'),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
// The wizard is its own modal, so hand over rather than stacking: close this window first.
|
|
38
|
+
function start() {
|
|
39
|
+
ui.closeProviderConnection()
|
|
40
|
+
ui.openEnvironmentSetup()
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<section class="space-y-3" data-testid="compose-env-setup-section">
|
|
46
|
+
<div>
|
|
47
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
48
|
+
{{ t('settings.composeEnvSetup.title') }}
|
|
49
|
+
</h3>
|
|
50
|
+
<p class="mt-1 text-xs leading-relaxed text-slate-400">
|
|
51
|
+
{{ t('settings.composeEnvSetup.lead') }}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="rounded border border-slate-800 bg-slate-900/40 p-3">
|
|
56
|
+
<p class="text-[11px] font-medium uppercase tracking-wide text-slate-400">
|
|
57
|
+
{{ t('settings.composeEnvSetup.how.title') }}
|
|
58
|
+
</p>
|
|
59
|
+
<ol class="mt-2 list-decimal space-y-1 ps-4 text-[11px] leading-relaxed text-slate-400">
|
|
60
|
+
<li v-for="(step, i) in steps" :key="`how-${i}`">{{ step }}</li>
|
|
61
|
+
</ol>
|
|
62
|
+
<p class="mt-2 text-[11px] leading-relaxed text-slate-500">
|
|
63
|
+
{{ t('settings.composeEnvSetup.how.outcome') }}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="grid gap-2 sm:grid-cols-2">
|
|
68
|
+
<div class="rounded border border-emerald-900/50 bg-emerald-950/20 p-3">
|
|
69
|
+
<p class="flex items-center gap-1.5 text-[11px] font-medium text-emerald-200/90">
|
|
70
|
+
<UIcon name="i-lucide-check" class="h-3.5 w-3.5 shrink-0" />
|
|
71
|
+
{{ t('settings.composeEnvSetup.needed.title') }}
|
|
72
|
+
</p>
|
|
73
|
+
<ul class="mt-1.5 list-disc space-y-1 ps-4 text-[11px] leading-relaxed text-slate-400">
|
|
74
|
+
<li v-for="(item, i) in needed" :key="`need-${i}`">{{ item }}</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="rounded border border-slate-800 bg-slate-900/40 p-3">
|
|
78
|
+
<p class="flex items-center gap-1.5 text-[11px] font-medium text-slate-300">
|
|
79
|
+
<UIcon name="i-lucide-minus" class="h-3.5 w-3.5 shrink-0" />
|
|
80
|
+
{{ t('settings.composeEnvSetup.notNeeded.title') }}
|
|
81
|
+
</p>
|
|
82
|
+
<ul class="mt-1.5 list-disc space-y-1 ps-4 text-[11px] leading-relaxed text-slate-400">
|
|
83
|
+
<li v-for="(item, i) in notNeeded" :key="`skip-${i}`">{{ item }}</li>
|
|
84
|
+
</ul>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="flex items-center gap-2">
|
|
89
|
+
<UButton
|
|
90
|
+
size="xs"
|
|
91
|
+
color="primary"
|
|
92
|
+
variant="soft"
|
|
93
|
+
icon="i-lucide-wand-sparkles"
|
|
94
|
+
data-testid="compose-env-setup-start"
|
|
95
|
+
@click="start()"
|
|
96
|
+
>
|
|
97
|
+
{{ t('settings.composeEnvSetup.start') }}
|
|
98
|
+
</UButton>
|
|
99
|
+
<span class="text-[11px] text-slate-500">{{ t('settings.composeEnvSetup.rerunHint') }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
</template>
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
// mode — the warm-container-pool + checkout-reuse settings (the local agent-container
|
|
7
7
|
// runtime, folded in from the former LocalModeSettingsPanel).
|
|
8
8
|
// - "Test environments" — where the Tester's ephemeral environments run. Shows the test-env
|
|
9
|
-
// backend selector
|
|
9
|
+
// backend selector, the environment-provider connection, and the guided per-service Docker
|
|
10
|
+
// Compose setup (formerly a standalone "Environment setup" sidebar entry — it writes a
|
|
11
|
+
// service's Compose recipe plus the workspace's Compose handler, so it belongs beside the
|
|
12
|
+
// settings it edits rather than at the same level as them).
|
|
10
13
|
// Local-specific affordances render inline, gated on `auth.localMode?.enabled`. A tab whose
|
|
11
14
|
// backend integration is disabled (503) simply doesn't render.
|
|
12
15
|
import { computed, ref, watch } from 'vue'
|
|
@@ -16,6 +19,7 @@ import InfraHandlersConfigurator from '~/components/settings/InfraHandlersConfig
|
|
|
16
19
|
import DefaultProvisionTypeSection from '~/components/settings/DefaultProvisionTypeSection.vue'
|
|
17
20
|
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
18
21
|
import SharedStacksPanel from '~/components/settings/SharedStacksPanel.vue'
|
|
22
|
+
import ComposeEnvironmentSetupSection from '~/components/settings/ComposeEnvironmentSetupSection.vue'
|
|
19
23
|
|
|
20
24
|
// The shared-stacks tab uses its own slot key beyond the provider-connection kinds.
|
|
21
25
|
type InfraTabValue = ProviderConnectionKind | 'shared-stacks'
|
|
@@ -142,6 +146,12 @@ watch([tabs, () => store.loaded], () => {
|
|
|
142
146
|
<div class="border-t border-slate-800 pt-4">
|
|
143
147
|
<InfraHandlersConfigurator />
|
|
144
148
|
</div>
|
|
149
|
+
<!-- The guided per-SERVICE Compose flow (formerly the standalone "Environment
|
|
150
|
+
setup" sidebar entry). Last, because it fills in one service's recipe on
|
|
151
|
+
top of the workspace-wide choices above. -->
|
|
152
|
+
<div class="border-t border-slate-800 pt-4">
|
|
153
|
+
<ComposeEnvironmentSetupSection />
|
|
154
|
+
</div>
|
|
145
155
|
</div>
|
|
146
156
|
</template>
|
|
147
157
|
<template #shared-stacks>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deleteServiceValidationConfigContract,
|
|
3
|
+
detectServiceValidationChecksContract,
|
|
3
4
|
getServiceValidationConfigContract,
|
|
4
5
|
listServiceValidationConfigsContract,
|
|
5
6
|
setServiceValidationConfigContract,
|
|
@@ -30,6 +31,13 @@ export function validationChecksApi({ send, ws }: ApiContext) {
|
|
|
30
31
|
body,
|
|
31
32
|
}),
|
|
32
33
|
|
|
34
|
+
/** Suggest checks from the service repo's manifests — a read; the operator still saves. */
|
|
35
|
+
detectServiceValidationChecks: (workspaceId: string, blockId: string) =>
|
|
36
|
+
send(detectServiceValidationChecksContract, {
|
|
37
|
+
pathPrefix: ws(workspaceId),
|
|
38
|
+
pathParams: { blockId },
|
|
39
|
+
}),
|
|
40
|
+
|
|
33
41
|
deleteServiceValidationConfig: (workspaceId: string, blockId: string) =>
|
|
34
42
|
send(deleteServiceValidationConfigContract, {
|
|
35
43
|
pathPrefix: ws(workspaceId),
|
|
@@ -10,8 +10,9 @@ import type { Ref, WritableComputedRef } from 'vue'
|
|
|
10
10
|
* resort to an obscure comma-operator expression. A named handler reads clearly instead.
|
|
11
11
|
*
|
|
12
12
|
* Returns to whichever hub the panel was reached from: the user-scoped "My setup" hub when
|
|
13
|
-
* `cameFromPersonal` is set,
|
|
14
|
-
*
|
|
13
|
+
* `cameFromPersonal` is set, the Model providers hub when `cameFromModelProviders` is set,
|
|
14
|
+
* else the workspace Integrations hub. A shared panel (e.g. the vendor-credentials modal,
|
|
15
|
+
* reachable from all three) thus lands the user back where they were.
|
|
15
16
|
*
|
|
16
17
|
* Pass the panel's `open` model (the writable ref/computed bound to its `UModal`).
|
|
17
18
|
*/
|
|
@@ -19,8 +20,10 @@ export function useIntegrationBack(open: Ref<boolean> | WritableComputedRef<bool
|
|
|
19
20
|
const ui = useUiStore()
|
|
20
21
|
return () => {
|
|
21
22
|
const toPersonal = ui.cameFromPersonal
|
|
23
|
+
const toModelProviders = ui.cameFromModelProviders
|
|
22
24
|
open.value = false
|
|
23
25
|
if (toPersonal) ui.openPersonalSetup()
|
|
26
|
+
else if (toModelProviders) ui.openModelProviders()
|
|
24
27
|
else ui.openIntegrations()
|
|
25
28
|
}
|
|
26
29
|
}
|
|
@@ -33,10 +33,10 @@ export function useNavContributions() {
|
|
|
33
33
|
addFromRepo: () => ui.openAddService(),
|
|
34
34
|
bootstrapRepo: () => ui.openBootstrap(),
|
|
35
35
|
integrationsHub: () => ui.openIntegrations(),
|
|
36
|
+
modelProviders: () => ui.openModelProviders(),
|
|
36
37
|
sandbox: () => ui.openSandbox(),
|
|
37
38
|
kaizen: () => ui.openKaizen(),
|
|
38
39
|
infrastructure: () => ui.openInfrastructure(),
|
|
39
|
-
environmentSetup: () => ui.openEnvironmentSetup(),
|
|
40
40
|
fragmentLibrary: () => ui.openFragmentLibrary(),
|
|
41
41
|
mergeThresholds: () => ui.openWorkspaceSettings('merge'),
|
|
42
42
|
workspaceSettings: () => ui.openWorkspaceSettings(),
|
|
@@ -99,12 +99,15 @@ describe('navSlotFilter', () => {
|
|
|
99
99
|
expect(kept).toContain('integrations-hub')
|
|
100
100
|
expect(kept).toContain('workspace-settings')
|
|
101
101
|
expect(kept).toContain('model-config')
|
|
102
|
+
// Model providers are NOT an integration: the split is what makes the engines findable.
|
|
103
|
+
expect(kept).toContain('model-providers')
|
|
102
104
|
// ...and so does everything the everyday delivery loop runs on, however deep it feels:
|
|
103
105
|
// authoring a flow, the standards library, and the PREnv/runner plumbing.
|
|
104
106
|
expect(kept).toContain('build-pipeline')
|
|
105
107
|
expect(kept).toContain('fragments')
|
|
108
|
+
// Also the only route to the guided per-service Compose environment setup, which folded
|
|
109
|
+
// into this window rather than staying a sibling nav entry.
|
|
106
110
|
expect(kept).toContain('infrastructure')
|
|
107
|
-
expect(kept).toContain('environment-setup')
|
|
108
111
|
// What drops is either a shortcut basic mode reaches another way, or a capability
|
|
109
112
|
// deliberately kept out of the tier (experimentation, one-off repo setup, the
|
|
110
113
|
// deployment-wide operator rollups).
|
|
@@ -134,7 +137,7 @@ describe('navSlotFilter', () => {
|
|
|
134
137
|
},
|
|
135
138
|
'local-models': {
|
|
136
139
|
kind: 'reached-another-way',
|
|
137
|
-
why: '
|
|
140
|
+
why: 'model-providers -> My local runners',
|
|
138
141
|
},
|
|
139
142
|
sandbox: {
|
|
140
143
|
kind: 'out-of-tier',
|
|
@@ -250,6 +253,7 @@ describe('NAV_CONTRIBUTIONS catalog integrity', () => {
|
|
|
250
253
|
for (const group of [
|
|
251
254
|
'create',
|
|
252
255
|
'repositories',
|
|
256
|
+
'models',
|
|
253
257
|
'integrations',
|
|
254
258
|
'infrastructure',
|
|
255
259
|
'workspaceContext',
|
|
@@ -275,15 +279,19 @@ describe('nav grouping helpers', () => {
|
|
|
275
279
|
expect(groups.map((g) => g.group)).toEqual([
|
|
276
280
|
'create',
|
|
277
281
|
'repositories',
|
|
282
|
+
'models',
|
|
278
283
|
'integrations',
|
|
279
284
|
'infrastructure',
|
|
280
285
|
'workspaceContext',
|
|
281
286
|
'configuration',
|
|
282
287
|
])
|
|
288
|
+
// The engines are their own section, ahead of the optional integrations; `model-config`
|
|
289
|
+
// sits beside the providers it picks models from rather than under `configuration`.
|
|
290
|
+
const models = groups.find((g) => g.group === 'models')
|
|
291
|
+
expect(models?.items.map((i) => i.id)).toEqual(['model-providers', 'model-config'])
|
|
283
292
|
const configuration = groups.find((g) => g.group === 'configuration')
|
|
284
293
|
expect(configuration?.items.map((i) => i.id)).toEqual([
|
|
285
294
|
'workspace-settings',
|
|
286
|
-
'model-config',
|
|
287
295
|
'account-settings',
|
|
288
296
|
'operator-dashboard',
|
|
289
297
|
'reports',
|
|
@@ -25,10 +25,21 @@ export type { AppSlots } from './slots'
|
|
|
25
25
|
/** Which shell(s) render a contribution. */
|
|
26
26
|
export type NavSurface = 'sidebar' | 'command' | 'toolbar'
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Sidebar section a contribution lands in (its i18n header is `nav.<group>`).
|
|
30
|
+
*
|
|
31
|
+
* `models` and `integrations` are deliberately SEPARATE sections even though a model
|
|
32
|
+
* provider is technically also an external system we connect to. They answer different
|
|
33
|
+
* questions: `models` is the ENGINE the harnesses run on (no provider ⇒ nothing runs at
|
|
34
|
+
* all), `integrations` is the optional systems that feed a run context or receive its
|
|
35
|
+
* output (source control, trackers, documents, chat, observability) — each of which a
|
|
36
|
+
* deployment can live without. Folding the providers in among them buried the one
|
|
37
|
+
* connection every deployment must make in a list of ones most never touch.
|
|
38
|
+
*/
|
|
29
39
|
export type NavSidebarGroup =
|
|
30
40
|
| 'create'
|
|
31
41
|
| 'repositories'
|
|
42
|
+
| 'models'
|
|
32
43
|
| 'integrations'
|
|
33
44
|
| 'infrastructure'
|
|
34
45
|
| 'workspaceContext'
|
|
@@ -90,10 +101,10 @@ export const NAV_ACTIONS = [
|
|
|
90
101
|
'addFromRepo',
|
|
91
102
|
'bootstrapRepo',
|
|
92
103
|
'integrationsHub',
|
|
104
|
+
'modelProviders',
|
|
93
105
|
'sandbox',
|
|
94
106
|
'kaizen',
|
|
95
107
|
'infrastructure',
|
|
96
|
-
'environmentSetup',
|
|
97
108
|
'fragmentLibrary',
|
|
98
109
|
'mergeThresholds',
|
|
99
110
|
'workspaceSettings',
|
|
@@ -164,7 +175,7 @@ const S = (...s: NavSurface[]) => s as readonly NavSurface[]
|
|
|
164
175
|
* - `merge-thresholds` / `service-fragment-defaults` — palette shortcuts into Workspace
|
|
165
176
|
* settings tabs (Merge, Service best practices), which basic mode reaches via
|
|
166
177
|
* `workspace-settings`.
|
|
167
|
-
* - `local-models` — a per-user endpoint knob the
|
|
178
|
+
* - `local-models` — a per-user endpoint knob the Model providers hub already offers.
|
|
168
179
|
*
|
|
169
180
|
* OUT OF THE TIER — the sole route, hidden deliberately, so the capability itself is absent
|
|
170
181
|
* from basic mode. This is a product decision, not an oversight: each of these answers a
|
|
@@ -180,8 +191,9 @@ const S = (...s: NavSurface[]) => s as readonly NavSurface[]
|
|
|
180
191
|
*
|
|
181
192
|
* Everything else stays in basic because the delivery loop needs it: authoring a flow
|
|
182
193
|
* (`build-pipeline`), adding a repo (`add-from-repo`), the standards/skills library
|
|
183
|
-
* (`fragments`), the PREnv + runner plumbing (`infrastructure`,
|
|
184
|
-
*
|
|
194
|
+
* (`fragments`), the PREnv + runner plumbing (`infrastructure`, which is also the only route
|
|
195
|
+
* to the guided per-service Compose environment setup), and the workspace/model configuration
|
|
196
|
+
* a run actually reads (`workspace-settings`, `model-config`).
|
|
185
197
|
*/
|
|
186
198
|
export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
187
199
|
{
|
|
@@ -233,6 +245,19 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
233
245
|
keywordsKey: 'layout.commandBar.keywords.bootstrapRepo',
|
|
234
246
|
},
|
|
235
247
|
},
|
|
248
|
+
{
|
|
249
|
+
// The engines. Kept out of `integrations-hub` on purpose (see NavSidebarGroup): a
|
|
250
|
+
// deployment with no provider connected cannot run anything, so this is the one
|
|
251
|
+
// connection that must not sit in a list of optional ones.
|
|
252
|
+
id: 'model-providers',
|
|
253
|
+
labelKey: 'nav.modelProviders',
|
|
254
|
+
icon: 'i-lucide-plug-zap',
|
|
255
|
+
surfaces: S('sidebar'),
|
|
256
|
+
gate: (g) => g.canManageIntegrations,
|
|
257
|
+
action: 'modelProviders',
|
|
258
|
+
testId: 'nav-model-providers',
|
|
259
|
+
sidebar: { group: 'models', order: 10 },
|
|
260
|
+
},
|
|
236
261
|
{
|
|
237
262
|
id: 'integrations-hub',
|
|
238
263
|
labelKey: 'nav.integrations',
|
|
@@ -280,16 +305,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
280
305
|
testId: 'nav-infrastructure',
|
|
281
306
|
sidebar: { group: 'infrastructure', order: 10 },
|
|
282
307
|
},
|
|
283
|
-
{
|
|
284
|
-
id: 'environment-setup',
|
|
285
|
-
labelKey: 'nav.environmentSetup',
|
|
286
|
-
icon: 'i-lucide-flask-conical',
|
|
287
|
-
surfaces: S('sidebar'),
|
|
288
|
-
gate: (g) => g.infrastructureAvailable,
|
|
289
|
-
action: 'environmentSetup',
|
|
290
|
-
testId: 'nav-environment-setup',
|
|
291
|
-
sidebar: { group: 'infrastructure', order: 20 },
|
|
292
|
-
},
|
|
293
308
|
{
|
|
294
309
|
id: 'fragments',
|
|
295
310
|
labelKey: 'nav.contextFragments',
|
|
@@ -344,7 +359,10 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
344
359
|
gate: (g) => g.canManageSettings,
|
|
345
360
|
action: 'modelConfiguration',
|
|
346
361
|
testId: 'nav-model-config',
|
|
347
|
-
|
|
362
|
+
// Beside `model-providers`, not in `configuration`: "which key do we hold" and "which
|
|
363
|
+
// model does each agent kind use" are two halves of one question, and splitting them
|
|
364
|
+
// across sections is what sent people to Integrations looking for a model.
|
|
365
|
+
sidebar: { group: 'models', order: 20 },
|
|
348
366
|
command: {
|
|
349
367
|
group: 'workspace',
|
|
350
368
|
order: 40,
|
|
@@ -491,6 +509,7 @@ export function navSlotFilter(slots: AppSlots, deps: { gates?: NavGates }): AppS
|
|
|
491
509
|
export const SIDEBAR_GROUP_ORDER: readonly NavSidebarGroup[] = [
|
|
492
510
|
'create',
|
|
493
511
|
'repositories',
|
|
512
|
+
'models',
|
|
494
513
|
'integrations',
|
|
495
514
|
'infrastructure',
|
|
496
515
|
'workspaceContext',
|
package/app/pages/index.vue
CHANGED
|
@@ -89,6 +89,9 @@ const ModelPresetHealthModal = defineAsyncComponent(
|
|
|
89
89
|
const IntegrationsHub = defineAsyncComponent(
|
|
90
90
|
() => import('~/components/layout/IntegrationsHub.vue'),
|
|
91
91
|
)
|
|
92
|
+
const ModelProvidersHub = defineAsyncComponent(
|
|
93
|
+
() => import('~/components/layout/ModelProvidersHub.vue'),
|
|
94
|
+
)
|
|
92
95
|
const PersonalSetupModal = defineAsyncComponent(
|
|
93
96
|
() => import('~/components/layout/PersonalSetupModal.vue'),
|
|
94
97
|
)
|
|
@@ -429,6 +432,7 @@ watch(
|
|
|
429
432
|
<RiskPolicyHealthModal v-if="ui.riskPolicyHealthOpen" />
|
|
430
433
|
<ModelPresetHealthModal v-if="ui.modelPresetHealthOpen" />
|
|
431
434
|
<IntegrationsHub v-if="ui.integrationsOpen" />
|
|
435
|
+
<ModelProvidersHub v-if="ui.modelProvidersOpen" />
|
|
432
436
|
<PersonalSetupModal v-if="ui.personalSetupOpen" />
|
|
433
437
|
<WorkspaceSettingsPanel v-if="ui.workspaceSettingsOpen" />
|
|
434
438
|
<AccountSettingsPanel v-if="ui.accountSettingsOpen" />
|
package/app/stores/documents.ts
CHANGED
|
@@ -106,12 +106,15 @@ export const useDocumentsStore = defineStore('documents', () => {
|
|
|
106
106
|
return api.planDocument(workspace.requireId(), source, externalId)
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Apply a page's structure to the board as new top-level frames, then refresh the
|
|
111
|
+
* board snapshot. The endpoint also accepts a `frameId` that flattens the planned
|
|
112
|
+
* frames into an existing service; the SPA deliberately never sends one, because the
|
|
113
|
+
* planner is target-blind and that path discards the frame titles/types the preview
|
|
114
|
+
* shows. Scoping a spawn to a service needs a target-aware plan first.
|
|
115
|
+
*/
|
|
116
|
+
async function spawn(source: DocumentSourceKind, externalId: string) {
|
|
117
|
+
const { result } = await api.spawnDocument(workspace.requireId(), source, { externalId })
|
|
115
118
|
await workspace.refresh()
|
|
116
119
|
return result
|
|
117
120
|
}
|