@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.
Files changed (36) hide show
  1. package/README.md +28 -1
  2. package/app/components/documents/DocumentImportModal.vue +3 -17
  3. package/app/components/documents/DocumentTemplatesModal.vue +1 -1
  4. package/app/components/documents/SpawnPreviewModal.vue +9 -19
  5. package/app/components/environments/steps/EnvPickStep.vue +3 -0
  6. package/app/components/layout/CommandBar.vue +1 -1
  7. package/app/components/layout/IntegrationBackTitle.vue +11 -8
  8. package/app/components/layout/IntegrationsHub.vue +22 -56
  9. package/app/components/layout/ModelProvidersHub.vue +250 -0
  10. package/app/components/panels/InspectorPanel.vue +2 -19
  11. package/app/components/panels/inspector/ServiceValidationConfig.vue +99 -1
  12. package/app/components/settings/ComposeEnvironmentSetupSection.vue +102 -0
  13. package/app/components/settings/InfrastructureWindow.vue +11 -1
  14. package/app/composables/api/validationChecks.ts +8 -0
  15. package/app/composables/useIntegrationBack.ts +5 -2
  16. package/app/composables/useNavContributions.ts +1 -1
  17. package/app/modular/nav-contributions.spec.ts +11 -3
  18. package/app/modular/nav-contributions.ts +35 -16
  19. package/app/pages/index.vue +4 -0
  20. package/app/stores/documents.ts +9 -6
  21. package/app/stores/ui/modals.ts +43 -27
  22. package/app/stores/validationChecks.ts +17 -2
  23. package/app/types/validationChecks.ts +3 -0
  24. package/app/utils/validationDetection.spec.ts +51 -0
  25. package/app/utils/validationDetection.ts +67 -0
  26. package/i18n/locales/de.json +103 -27
  27. package/i18n/locales/en.json +103 -27
  28. package/i18n/locales/es.json +103 -27
  29. package/i18n/locales/fr.json +103 -27
  30. package/i18n/locales/he.json +103 -27
  31. package/i18n/locales/it.json +103 -27
  32. package/i18n/locales/ja.json +103 -27
  33. package/i18n/locales/pl.json +103 -27
  34. package/i18n/locales/tr.json +103 -27
  35. package/i18n/locales/uk.json +103 -27
  36. package/package.json +2 -2
@@ -156,23 +156,20 @@ function createMiscModals() {
156
156
  * came-from markers (they can be reached from the Integrations hub).
157
157
  */
158
158
  function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
159
- // Document-source integration modals, keyed by source. `documentImport` and
160
- // `spawnPreview` carry an optional target frame, so structure spawned from a
161
- // frame's inspector lands inside that frame rather than creating new top-level
162
- // frames. `documentConnect` carries the source whose connect form to show;
163
- // `documentImport`'s source may be null to let the modal pick a connected one.
159
+ // Document-source integration modals, keyed by source. A spawn always creates new
160
+ // top-level frames: the planner decomposes a document into services, so spawning
161
+ // into an existing frame could only flatten those away (discarding the frame titles
162
+ // and types the preview shows). `documentConnect` carries the source whose connect
163
+ // form to show; `documentImport`'s source may be null to let the modal pick a
164
+ // connected one.
164
165
  const documentConnect = ref<{ source: DocumentSourceKind } | null>(null)
165
- const documentImport = ref<{
166
- source: DocumentSourceKind | null
167
- targetFrameId: string | null
168
- } | null>(null)
166
+ const documentImport = ref<{ source: DocumentSourceKind | null } | null>(null)
169
167
  // The workspace+DocKind template / exemplar management modal (WS1). A single boolean —
170
168
  // it manages every kind's links in one place.
171
169
  const documentTemplates = ref(false)
172
170
  const spawnPreview = ref<{
173
171
  source: DocumentSourceKind
174
172
  externalId: string
175
- targetFrameId: string | null
176
173
  } | null>(null)
177
174
 
178
175
  // Task-source integration modals, keyed by source. `taskConnect` carries the
@@ -222,12 +219,9 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
222
219
  function closeDocumentConnect() {
223
220
  documentConnect.value = null
224
221
  }
225
- function openDocumentImport(
226
- targetFrameId: string | null = null,
227
- source: DocumentSourceKind | null = null,
228
- ) {
222
+ function openDocumentImport(source: DocumentSourceKind | null = null) {
229
223
  resetHubReturn()
230
- documentImport.value = { source, targetFrameId }
224
+ documentImport.value = { source }
231
225
  }
232
226
  function closeDocumentImport() {
233
227
  documentImport.value = null
@@ -239,12 +233,8 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
239
233
  function closeDocumentTemplates() {
240
234
  documentTemplates.value = false
241
235
  }
242
- function openSpawnPreview(
243
- source: DocumentSourceKind,
244
- externalId: string,
245
- targetFrameId: string | null = null,
246
- ) {
247
- spawnPreview.value = { source, externalId, targetFrameId }
236
+ function openSpawnPreview(source: DocumentSourceKind, externalId: string) {
237
+ spawnPreview.value = { source, externalId }
248
238
  }
249
239
  function closeSpawnPreview() {
250
240
  spawnPreview.value = null
@@ -952,20 +942,28 @@ function createConsumerOverlayHost() {
952
942
  * the shared hub came-from markers.
953
943
  */
954
944
  export function createUiModals() {
955
- // Integrations / My-setup hub came-from markers — the one piece of state SHARED across slices
956
- // (many `open*` handlers reset it), so it lives here and `resetHubReturn` is threaded into the
957
- // slices that need it. `cameFromIntegrations` is true while an integration's own panel is showing
958
- // AND it was reached from the Integrations hub; `cameFromPersonal` is the My-setup analogue.
945
+ // Model-providers / Integrations / My-setup hub came-from markers — the one piece of state
946
+ // SHARED across slices (many `open*` handlers reset it), so it lives here and `resetHubReturn`
947
+ // is threaded into the slices that need it. `cameFromIntegrations` is true while an
948
+ // integration's own panel is showing AND it was reached from the Integrations hub;
949
+ // `cameFromModelProviders` and `cameFromPersonal` are the analogues for the other two hubs.
950
+ //
951
+ // Three hubs, not two, because the vendor-credentials / OpenRouter / local-runner panels are
952
+ // reachable from more than one of them and a Back control that lands somewhere the user was
953
+ // never at is worse than none.
959
954
  const cameFromIntegrations = ref(false)
955
+ const cameFromModelProviders = ref(false)
960
956
  const cameFromPersonal = ref(false)
961
957
  const integrationsOpen = ref(false)
958
+ const modelProvidersOpen = ref(false)
962
959
  const personalSetupOpen = ref(false)
963
960
 
964
- // Clear BOTH hub came-from markers. Every direct `open*` in the slices calls this so that a
961
+ // Clear EVERY hub came-from marker. Every direct `open*` in the slices calls this so that a
965
962
  // panel opened outside the hubs never grows a dead Back control, and so switching from one
966
- // hub's panel to the other's clears the stale marker.
963
+ // hub's panel to another's clears the stale marker.
967
964
  function resetHubReturn() {
968
965
  cameFromIntegrations.value = false
966
+ cameFromModelProviders.value = false
969
967
  cameFromPersonal.value = false
970
968
  }
971
969
  function openIntegrations() {
@@ -977,6 +975,13 @@ export function createUiModals() {
977
975
  function closeIntegrations() {
978
976
  integrationsOpen.value = false
979
977
  }
978
+ function openModelProviders() {
979
+ resetHubReturn()
980
+ modelProvidersOpen.value = true
981
+ }
982
+ function closeModelProviders() {
983
+ modelProvidersOpen.value = false
984
+ }
980
985
  function openPersonalSetup() {
981
986
  resetHubReturn()
982
987
  personalSetupOpen.value = true
@@ -1000,6 +1005,12 @@ export function createUiModals() {
1000
1005
  cameFromIntegrations.value = true
1001
1006
  integrationsOpen.value = false
1002
1007
  }
1008
+ // The Model-providers analogue of `openFromIntegrations`.
1009
+ function openFromModelProviders(open: () => void) {
1010
+ open()
1011
+ cameFromModelProviders.value = true
1012
+ modelProvidersOpen.value = false
1013
+ }
1003
1014
 
1004
1015
  const health = createHealthAdvisoryModals()
1005
1016
  const misc = createMiscModals()
@@ -1014,11 +1025,16 @@ export function createUiModals() {
1014
1025
  return {
1015
1026
  integrationsOpen,
1016
1027
  cameFromIntegrations,
1028
+ modelProvidersOpen,
1029
+ cameFromModelProviders,
1017
1030
  personalSetupOpen,
1018
1031
  cameFromPersonal,
1019
1032
  openIntegrations,
1020
1033
  closeIntegrations,
1021
1034
  openFromIntegrations,
1035
+ openModelProviders,
1036
+ closeModelProviders,
1037
+ openFromModelProviders,
1022
1038
  openPersonalSetup,
1023
1039
  closePersonalSetup,
1024
1040
  openFromPersonal,
@@ -1,6 +1,10 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
- import type { ServiceValidationConfig, ValidationCheck } from '~/types/validationChecks'
3
+ import type {
4
+ DetectedValidationChecks,
5
+ ServiceValidationConfig,
6
+ ValidationCheck,
7
+ } from '~/types/validationChecks'
4
8
  import { VALIDATION_DEFAULT_MAX_ATTEMPTS } from '~/types/validationChecks'
5
9
  import { useUpsertList } from '~/composables/useUpsertList'
6
10
  import { useWorkspaceStore } from '~/stores/workspace'
@@ -107,5 +111,16 @@ export const useValidationChecksStore = defineStore('validationChecks', () => {
107
111
  dropLocal(blockId)
108
112
  }
109
113
 
110
- return { configs, loading, available, load, ensureLoaded, forBlock, save, remove }
114
+ /**
115
+ * Ask the backend what this service's repo implies. Nothing is stored: the suggestion goes
116
+ * into the panel's unsaved rows, so the operator reviews and saves it like anything they
117
+ * typed. Deliberately NOT folded into `configs` — a suggestion that looked like saved
118
+ * config would report a service as configured when no run would run a single command.
119
+ */
120
+ async function detect(blockId: string): Promise<DetectedValidationChecks> {
121
+ const ws = useWorkspaceStore()
122
+ return api.detectServiceValidationChecks(ws.requireId(), blockId)
123
+ }
124
+
125
+ return { configs, loading, available, load, ensureLoaded, forBlock, save, remove, detect }
111
126
  })
@@ -9,6 +9,9 @@ export type {
9
9
  ValidationReport,
10
10
  ServiceValidationConfig,
11
11
  UpsertServiceValidationConfigInput,
12
+ DetectedValidationChecks,
13
+ ValidationDetectionStatus,
14
+ ValidationEcosystem,
12
15
  } from '@cat-factory/contracts'
13
16
  export {
14
17
  VALIDATION_DEFAULT_MAX_ATTEMPTS,
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mergeDetectedChecks } from './validationDetection'
3
+
4
+ const check = (label: string, command: string) => ({ label, command })
5
+
6
+ describe('mergeDetectedChecks', () => {
7
+ it('appends suggestions to an empty (or blank-row) panel', () => {
8
+ const result = mergeDetectedChecks(
9
+ [check('', '')],
10
+ [check('install', 'pnpm install --frozen-lockfile'), check('lint', 'pnpm run lint')],
11
+ 10,
12
+ )
13
+ expect(result.rows).toEqual([
14
+ check('install', 'pnpm install --frozen-lockfile'),
15
+ check('lint', 'pnpm run lint'),
16
+ ])
17
+ expect(result.added).toBe(2)
18
+ expect(result.dropped).toBe(0)
19
+ })
20
+
21
+ it('never rewrites or duplicates what the operator already has', () => {
22
+ // The same command with a hand-tuned label must survive untouched, and pressing Detect
23
+ // again must be a no-op rather than a second copy of every check.
24
+ const existing = [check('our lint', 'pnpm run lint')]
25
+ const result = mergeDetectedChecks(existing, [check('lint', 'pnpm run lint')], 10)
26
+ expect(result.rows).toEqual(existing)
27
+ expect(result.added).toBe(0)
28
+ })
29
+
30
+ it('disambiguates a label that collides with an existing row', () => {
31
+ // A duplicate label is REJECTED by the write contract, so a collision the merge created
32
+ // would surface as a save error on a row the operator never typed.
33
+ const result = mergeDetectedChecks(
34
+ [check('test', 'make test')],
35
+ [check('test', 'go test ./...')],
36
+ 10,
37
+ )
38
+ expect(result.rows.map((r) => r.label)).toEqual(['test', 'test 2'])
39
+ })
40
+
41
+ it('reports what the row cap dropped instead of silently truncating', () => {
42
+ const result = mergeDetectedChecks(
43
+ [check('lint', 'pnpm run lint')],
44
+ [check('test', 'pnpm run test'), check('build', 'pnpm run build')],
45
+ 2,
46
+ )
47
+ expect(result.rows).toHaveLength(2)
48
+ expect(result.added).toBe(1)
49
+ expect(result.dropped).toBe(1)
50
+ })
51
+ })
@@ -0,0 +1,67 @@
1
+ import type { ValidationCheck } from '~/types/validationChecks'
2
+
3
+ // Folding an autodetected suggestion into the pre-PR validation panel's UNSAVED rows.
4
+ //
5
+ // Pure, and separate from the panel, because the merge carries the two rules that make the
6
+ // button safe to press: it never destroys what the operator already typed, and it never
7
+ // produces a row set the write contract would reject.
8
+
9
+ /** What a merge did, so the panel can say something specific rather than "done". */
10
+ export interface MergedDetection {
11
+ rows: ValidationCheck[]
12
+ /** Suggestions appended as new rows. */
13
+ added: number
14
+ /** Suggestions dropped because the row cap was already reached. */
15
+ dropped: number
16
+ }
17
+
18
+ /**
19
+ * Append detected checks to the rows already on screen.
20
+ *
21
+ * - A suggestion whose COMMAND is already present is skipped, so pressing Detect twice (or
22
+ * pressing it on a service that is already configured) adds nothing rather than
23
+ * duplicating every check.
24
+ * - Existing rows are never rewritten. The button is assistive; silently replacing an
25
+ * operator's hand-tuned command with the generic guess would be a worse failure than not
26
+ * detecting anything.
27
+ * - Wholly blank rows (an unused "Add check" click) are dropped first, so the suggestion
28
+ * lands in a contiguous list instead of after a gap.
29
+ * - Labels are made unique against the surviving rows, because the write contract REJECTS a
30
+ * duplicate label — a merge that produced one would surface as an unexplained save error
31
+ * on rows the operator did not write.
32
+ */
33
+ export function mergeDetectedChecks(
34
+ existing: ValidationCheck[],
35
+ detected: ValidationCheck[],
36
+ maxRows: number,
37
+ ): MergedDetection {
38
+ const rows = existing.filter((r) => r.label.trim() !== '' || r.command.trim() !== '')
39
+ const commands = new Set(rows.map((r) => r.command.trim()))
40
+ const labels = new Set(rows.map((r) => r.label.trim()).filter((l) => l !== ''))
41
+
42
+ let added = 0
43
+ let dropped = 0
44
+ for (const candidate of detected) {
45
+ const command = candidate.command.trim()
46
+ if (command === '' || commands.has(command)) continue
47
+ if (rows.length >= maxRows) {
48
+ dropped += 1
49
+ continue
50
+ }
51
+ const label = uniqueLabel(candidate.label.trim() || command, labels)
52
+ rows.push({ label, command })
53
+ commands.add(command)
54
+ labels.add(label)
55
+ added += 1
56
+ }
57
+ return { rows, added, dropped }
58
+ }
59
+
60
+ /** `lint` → `lint 2` → `lint 3` … until it no longer collides. Clamped to the contract's 80. */
61
+ function uniqueLabel(base: string, taken: Set<string>): string {
62
+ if (!taken.has(base)) return base.slice(0, 80)
63
+ for (let n = 2; ; n += 1) {
64
+ const candidate = `${base} ${n}`.slice(0, 80)
65
+ if (!taken.has(candidate)) return candidate
66
+ }
67
+ }
@@ -658,6 +658,32 @@
658
658
  "removeFailed": "Der gemeinsame Stack konnte nicht gelöscht werden"
659
659
  }
660
660
  },
661
+ "composeEnvSetup": {
662
+ "title": "Geführte Docker-Compose-Einrichtung für einen Service",
663
+ "lead": "Führt einen Service Schritt für Schritt durch das Erstellen des Docker-Compose-Rezepts, aus dem seine Testumgebungen aufgebaut werden, statt Compose-Pfad und Setup-Schritte von Hand zu schreiben.",
664
+ "how": {
665
+ "title": "So funktioniert es",
666
+ "scan": "Durchsucht das verknüpfte Repository des Service nach Compose-Dateien, Profilen, Env-Dateien und Seed-Dumps.",
667
+ "analyse": "Führt optional den Umgebungsanalysten aus, der Setup-Schritte und ein Health-Gate aus den Skripten des Repositorys entwirft.",
668
+ "preflight": "Führt die Preflight-Prüfungen des Rezepts auf der Maschine aus, die es bereitstellen wird.",
669
+ "save": "Speichert das Rezept am Service und registriert den Docker-Compose-Handler des Workspace.",
670
+ "outcome": "Danach stellt der Deployer genau diesen Stack bereit, sobald eine Pipeline eine laufende Umgebung braucht, und der Tester testet dagegen. Bis zum Speichern wird nichts übernommen."
671
+ },
672
+ "needed": {
673
+ "title": "Sie brauchen das, wenn",
674
+ "running": "Die Tests des Service die laufende Anwendung benötigen: API- oder UI-Tests, Migrationen, Seed-Daten.",
675
+ "compose": "Der Service über Docker Compose bereitgestellt wird und noch kein Rezept gespeichert hat.",
676
+ "setup": "Das Hochfahren des Stacks mehr als einen Befehl erfordert: Profile, Env-Dateien, Seed-Daten oder eine Bereitschaftsprüfung."
677
+ },
678
+ "notNeeded": {
679
+ "title": "Sie können es überspringen, wenn",
680
+ "inContainer": "Die Pipeline nur lintet, baut oder Unit-Tests ausführt. Das läuft als Pre-PR-Validierung im Agenten-Container, ohne dass eine Umgebung bereitgestellt wird.",
681
+ "otherBackend": "Der Service anders bereitgestellt wird (Kubernetes, eine Cloud-Vorschau oder gar keine Infrastruktur). Konfigurieren Sie das stattdessen oben.",
682
+ "noRepo": "Der Service kein verknüpftes Repository hat, es also nichts zu durchsuchen gibt."
683
+ },
684
+ "start": "Geführte Einrichtung starten",
685
+ "rerunHint": "Jederzeit erneut ausführbar, um ein gespeichertes Rezept zu ändern."
686
+ },
661
687
  "localMode": {
662
688
  "title": "Lokaler Modus",
663
689
  "intro": "Feinabstimmung für den lokalen Container-Runner, gespeichert im Deployment dieser Maschine (er hat die Umgebungsvariablen {poolVars} / {harnessVars} ersetzt). Das Speichern passt den Warm-Pool live an, kein Neustart nötig; laufende Ausführungen behalten den Container, den sie bereits halten.",
@@ -1419,7 +1445,34 @@
1419
1445
  "save": "Prüfungen speichern",
1420
1446
  "savedToast": "Prüfungen gespeichert",
1421
1447
  "saveFailed": "Prüfungen konnten nicht gespeichert werden",
1422
- "clearFailed": "Prüfungen konnten nicht geleert werden"
1448
+ "clearFailed": "Prüfungen konnten nicht geleert werden",
1449
+ "detect": {
1450
+ "action": "Erkennen",
1451
+ "hint": "Prüfungen aus dem Repository dieses Dienstes vorschlagen",
1452
+ "added": "{count} Prüfung hinzugefügt | {count} Prüfungen hinzugefügt",
1453
+ "found": "Erkannt: {ecosystems}.",
1454
+ "capped": "Einige Vorschläge wurden ausgelassen — ein Dienst nimmt höchstens {max} Prüfungen auf.",
1455
+ "nothingNew": "Nichts hinzuzufügen",
1456
+ "alreadyPresent": "Alle erkannten Befehle stehen bereits in der Liste.",
1457
+ "unrecognised": "Im Wurzelverzeichnis des Repositorys wurde kein bekanntes Ökosystem erkannt. Bitte die Befehle von Hand eintragen.",
1458
+ "repo_unavailable": "Diesem Dienst ist kein Repository zugeordnet, es gibt also nichts zu untersuchen.",
1459
+ "failed": "Das Repository konnte nicht gelesen werden"
1460
+ },
1461
+ "ecosystem": {
1462
+ "node": "Node.js",
1463
+ "python": "Python",
1464
+ "go": "Go",
1465
+ "rust": "Rust",
1466
+ "maven": "Maven",
1467
+ "gradle": "Gradle",
1468
+ "dotnet": ".NET",
1469
+ "ruby": "Ruby",
1470
+ "php": "PHP",
1471
+ "elixir": "Elixir",
1472
+ "make": "Make",
1473
+ "just": "just",
1474
+ "task": "Task"
1475
+ }
1423
1476
  }
1424
1477
  },
1425
1478
  "panels": {
@@ -1658,7 +1711,6 @@
1658
1711
  "lastIncorporatedRequirements": "Zuletzt eingearbeitete Anforderungen",
1659
1712
  "priorDocHint": "Aus der vorherigen Prüfung. Verwende es als Basis: bearbeite die Beschreibung oben und reiche erneut ein.",
1660
1713
  "bootstrapping": "Wird initialisiert…",
1661
- "spawnFromDocument": "Aus Dokument erzeugen",
1662
1714
  "viewRequirements": "Anforderungen anzeigen",
1663
1715
  "reRun": "Erneut ausführen",
1664
1716
  "run": "Ausführen",
@@ -1975,7 +2027,8 @@
1975
2027
  },
1976
2028
  "integrationBack": {
1977
2029
  "backToMySetup": "Zurück zu Meine Einrichtung",
1978
- "backToIntegrations": "Zurück zu Integrationen"
2030
+ "backToIntegrations": "Zurück zu Integrationen",
2031
+ "backToModelProviders": "Zurück zu Modellanbieter"
1979
2032
  },
1980
2033
  "commandBar": {
1981
2034
  "searchPlaceholder": "Suchen oder einen Befehl ausführen…",
@@ -2043,23 +2096,21 @@
2043
2096
  },
2044
2097
  "integrationsHub": {
2045
2098
  "title": "Integrationen",
2046
- "intro": "Verbinden und verwalten Sie die externen Systeme, die dieser Workspace einbinden kann.",
2099
+ "intro": "Verbinden und verwalten Sie die optionalen externen Systeme, die dieser Workspace einbinden kann. Die Modelle, auf denen Ihre Agenten laufen, finden Sie unter Modellanbieter.",
2047
2100
  "searchPlaceholder": "Integrationen durchsuchen…",
2048
2101
  "noMatches": "Keine Integrationen passen zu \"{query}\".",
2049
2102
  "getStarted": {
2050
2103
  "title": "Erste Schritte",
2051
- "body": "Verbinden Sie eine Code-Quelle und einen Modellanbieter, um Ihre erste Pipeline auszuführen."
2104
+ "body": "Verbinden Sie eine Code-Quelle, damit Ihre Pipelines etwas zu bearbeiten haben."
2052
2105
  },
2053
2106
  "status": {
2054
2107
  "connected": "Verbunden",
2055
- "keyConnected": "Schlüssel verbunden",
2056
2108
  "notConnected": "Nicht verbunden",
2057
2109
  "needsAttention": "Benötigt Aufmerksamkeit",
2058
2110
  "disabled": "Deaktiviert",
2059
2111
  "recommended": "Empfohlen"
2060
2112
  },
2061
2113
  "groups": {
2062
- "models": "Modelle und Anbieter",
2063
2114
  "sourceControl": "Versionsverwaltung",
2064
2115
  "communication": "Kommunikation",
2065
2116
  "documents": "Dokumente",
@@ -2069,16 +2120,6 @@
2069
2120
  "personal": "Persönlich (nur Sie)"
2070
2121
  },
2071
2122
  "items": {
2072
- "openrouter": {
2073
- "description": "Ein Gateway zu über 300 Modellen. Fügen Sie Ihren Schlüssel hinzu und aktivieren Sie Modelle an einem Ort."
2074
- },
2075
- "vendors": {
2076
- "label": "Anbieter und Schlüssel",
2077
- "description": "Workspace-LLM-Abonnements und Anbieter-API-Schlüssel."
2078
- },
2079
- "personalSubs": {
2080
- "label": "Persönliche Abonnements (Claude, ChatGPT/Codex, GLM)"
2081
- },
2082
2123
  "github": {
2083
2124
  "description": "Verbinden Sie die GitHub-App des Workspace, durchsuchen Sie Repos, PRs und Issues."
2084
2125
  },
@@ -2118,10 +2159,6 @@
2118
2159
  "label": "Mein GitHub-Token",
2119
2160
  "description": "Ein Personal Access Token, der für von Ihnen gestartete Läufe verwendet wird (Pushes, PRs, CI, Merge)."
2120
2161
  },
2121
- "localRunners": {
2122
- "label": "Meine lokalen Runner",
2123
- "description": "Modell-Runner auf Ihrer eigenen Maschine (Ollama, LM Studio, vLLM…)."
2124
- },
2125
2162
  "documentTemplates": {
2126
2163
  "label": "Vorlagen & Beispiele"
2127
2164
  },
@@ -2131,6 +2168,45 @@
2131
2168
  }
2132
2169
  }
2133
2170
  },
2171
+ "modelProvidersHub": {
2172
+ "title": "Modellanbieter",
2173
+ "intro": "Die Engines, auf denen Ihre Agenten laufen. Mindestens ein Anbieter muss verbunden sein, bevor ein Pipeline-Schritt ausgeführt werden kann.",
2174
+ "searchPlaceholder": "Anbieter suchen…",
2175
+ "noMatches": "Keine Anbieter entsprechen \"{query}\".",
2176
+ "getStarted": {
2177
+ "title": "Noch kein Anbieter verbunden",
2178
+ "body": "Agentenschritte können erst laufen, wenn ein Modell erreichbar ist. Ein OpenRouter-Schlüssel ist der schnellste Weg zu einem großen Katalog, aber Anbieterschlüssel, ein persönliches Abonnement oder Ihr eigener lokaler Runner funktionieren genauso gut."
2179
+ },
2180
+ "status": {
2181
+ "connected": "Verbunden",
2182
+ "keyConnected": "Schlüssel verbunden",
2183
+ "notConnected": "Nicht verbunden",
2184
+ "recommended": "Empfohlen",
2185
+ "connectedCount": "keine verbunden | {count} verbunden | {count} verbunden"
2186
+ },
2187
+ "groups": {
2188
+ "workspace": "Workspace-Anbieter",
2189
+ "personal": "Persönlich (nur Sie)",
2190
+ "personalNote": "Diese gehören zu Ihrem Konto und nicht zum Workspace und werden nur für Läufe verwendet, die Sie starten. Sie sind auch unter Meine Einrichtung aufgeführt."
2191
+ },
2192
+ "items": {
2193
+ "openrouter": {
2194
+ "description": "Ein Gateway zu über 300 Modellen. Fügen Sie Ihren Schlüssel hinzu und aktivieren Sie Modelle an einem Ort."
2195
+ },
2196
+ "vendors": {
2197
+ "label": "Anbieter und Schlüssel",
2198
+ "description": "Workspace-LLM-Abonnements und Anbieter-API-Schlüssel."
2199
+ },
2200
+ "personalSubs": {
2201
+ "label": "Meine Abonnements",
2202
+ "description": "Pläne zur individuellen Nutzung, die Sie mit einem persönlichen Passwort freischalten (Claude, GLM, Codex)."
2203
+ },
2204
+ "localRunners": {
2205
+ "label": "Meine lokalen Runner",
2206
+ "description": "Modell-Runner auf Ihrer eigenen Maschine (Ollama, LM Studio, vLLM)."
2207
+ }
2208
+ }
2209
+ },
2134
2210
  "accountSkills": {
2135
2211
  "intro": "Verknüpfe Repositorys, in denen dein Team Claude Skills erstellt (ein SKILL.md-Ordner plus Ressourcendateien). Synchronisierte Skills landen im Katalog dieses Kontos, der von allen Boards geteilt wird, und der Skill-Schritt einer Pipeline führt einen davon für eine Aufgabe aus."
2136
2212
  },
@@ -3209,7 +3285,6 @@
3209
3285
  "title": "Aus einer Dokumentquelle importieren",
3210
3286
  "connectFirst": "Verbinden Sie zuerst eine Dokumentquelle.",
3211
3287
  "connectSource": "{source} verbinden",
3212
- "spawningInto": "Wird in {frame} erzeugt",
3213
3288
  "sourceLabel": "Quelle",
3214
3289
  "refLabel": "Seiten-URL oder -ID",
3215
3290
  "importButton": "Importieren",
@@ -3234,7 +3309,6 @@
3234
3309
  "title": "Struktur-Vorschau",
3235
3310
  "plannerLlm": "KI-generiert",
3236
3311
  "plannerHeadings": "Aus Überschriften",
3237
- "intoFrame": "in {frame}",
3238
3312
  "asTopLevel": "als neue Frames auf oberster Ebene",
3239
3313
  "buildingPlan": "Plan wird erstellt…",
3240
3314
  "spawnOntoBoard": "Auf dem Board erzeugen",
@@ -4356,6 +4430,8 @@
4356
4430
  "repositories": "Repositories",
4357
4431
  "addFromRepo": "Aus vorhandenem Repo hinzufügen",
4358
4432
  "bootstrapRepo": "Repo bootstrappen",
4433
+ "models": "Modelle",
4434
+ "modelProviders": "Modellanbieter",
4359
4435
  "integrations": "Integrationen",
4360
4436
  "infrastructure": "Infrastruktur",
4361
4437
  "sandbox": "Sandbox",
@@ -4367,8 +4443,7 @@
4367
4443
  "modelConfiguration": "Modellkonfiguration",
4368
4444
  "accountSettings": "Kontoeinstellungen",
4369
4445
  "operatorDashboard": "Plattform-Observability",
4370
- "reports": "Berichte",
4371
- "environmentSetup": "Umgebungseinrichtung"
4446
+ "reports": "Berichte"
4372
4447
  },
4373
4448
  "errors": {
4374
4449
  "action": {
@@ -5263,8 +5338,8 @@
5263
5338
  },
5264
5339
  "environmentWizard": {
5265
5340
  "progress": "Schritt {index} von {total}",
5266
- "title": "Eine Testumgebung einrichten",
5267
- "subtitle": "Erkenne, prüfe und speichere ein Docker-Compose-Rezept, damit der Deployer es bereitstellt.",
5341
+ "title": "Docker-Compose-Umgebungseinrichtung",
5342
+ "subtitle": "Erstellen Sie das Docker-Compose-Rezept, aus dem die Testumgebungen eines Service aufgebaut werden: Repository durchsuchen, prüfen, Preflight, speichern.",
5268
5343
  "recommended": "empfohlen",
5269
5344
  "steps": {
5270
5345
  "pick": "Service auswählen",
@@ -5274,6 +5349,7 @@
5274
5349
  },
5275
5350
  "pick": {
5276
5351
  "intro": "Wähle den Service, für den eine Docker-Compose-Umgebung eingerichtet werden soll.",
5352
+ "scope": "Nur der ausgewählte Service wird geändert. Führen Sie dies später erneut aus, um sein Rezept zu bearbeiten.",
5277
5353
  "empty": "Noch keine Services. Füge zuerst einen Service hinzu."
5278
5354
  },
5279
5355
  "review": {