@cat-factory/app 0.215.1 → 0.216.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 +109 -1
- package/app/components/bootstrap/BootstrapModal.vue +65 -44
- package/app/components/github/AddServiceFromRepoModal.vue +49 -27
- package/app/components/github/GitHubOnboarding.vue +4 -23
- package/app/components/github/GitHubPanel.vue +10 -34
- package/app/components/inputGate/InputGateNotice.vue +176 -0
- package/app/components/panels/AgentStepDetail.vue +27 -3
- package/app/components/panels/inspector/TaskExecution.vue +32 -3
- package/app/components/pipeline/PipelineProgress.vue +1 -1
- package/app/components/settings/WorkspaceSettingsPanel.vue +34 -1
- package/app/components/vcs/VcsConnectSurfaces.vue +58 -0
- package/app/composables/api/inputGate.ts +25 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/usePipelineErrorToast.ts +8 -0
- package/app/stores/github/vcsConnect.ts +62 -0
- package/app/stores/github.spec.ts +31 -0
- package/app/stores/github.ts +5 -26
- package/app/stores/inputGate.ts +58 -0
- package/app/stores/ui/resultViews.ts +9 -1
- package/app/stores/workspaceSettings.ts +1 -0
- package/app/types/domain.ts +1 -0
- package/app/utils/inputGate.spec.ts +52 -0
- package/app/utils/inputGate.ts +44 -0
- package/app/utils/pipelineRender.spec.ts +47 -9
- package/app/utils/pipelineRender.ts +23 -2
- package/app/utils/vcs.spec.ts +101 -0
- package/app/utils/vcs.ts +63 -1
- package/i18n/locales/de.json +76 -17
- package/i18n/locales/en.json +76 -17
- package/i18n/locales/es.json +76 -17
- package/i18n/locales/fr.json +76 -17
- package/i18n/locales/he.json +76 -17
- package/i18n/locales/it.json +76 -17
- package/i18n/locales/ja.json +76 -17
- package/i18n/locales/pl.json +76 -17
- package/i18n/locales/tr.json +76 -17
- package/i18n/locales/uk.json +76 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,6 +18,9 @@ The SPA source lives under `app/` (the Nuxt srcDir).
|
|
|
18
18
|
- [Interface modes (basic / advanced)](#interface-modes-basic--advanced)
|
|
19
19
|
- [Agent tiers (basic / intermediate / advanced)](#agent-tiers-basic--intermediate--advanced)
|
|
20
20
|
- [In-app tutorial tours](#in-app-tutorial-tours)
|
|
21
|
+
- [Real-time store coherence](#real-time-store-coherence-avoid-the-full-refresh-clobber)
|
|
22
|
+
- [Internationalization (i18n) authoring](#internationalization-i18n-authoring)
|
|
23
|
+
- [Extending the layer (consumer modules)](#extending-the-layer-consumer-modules)
|
|
21
24
|
- [Key UI surfaces](#key-ui-surfaces)
|
|
22
25
|
- [Develop & test](#develop--test)
|
|
23
26
|
|
|
@@ -329,7 +332,7 @@ is SUPPRESSED rather than unmounted, because it holds the running tour's resolve
|
|
|
329
332
|
remount would re-resolve it against gates that may have flipped since the tour started.
|
|
330
333
|
|
|
331
334
|
The decisions behind this surface, and why each alternative was rejected, are recorded in
|
|
332
|
-
[ADR
|
|
335
|
+
[ADR 0036](../../backend/docs/adr/0036-in-app-tutorials.md). This section is the authority on how
|
|
333
336
|
the thing WORKS.
|
|
334
337
|
|
|
335
338
|
A tour is **data, not components**: an ordered list of steps, each pointing at an on-screen
|
|
@@ -511,6 +514,111 @@ guard scans the layer for both ways an id is named: written onto an element, or
|
|
|
511
514
|
`testId` field on a data contribution (the whole `nav-*` family reaches the DOM that way). It
|
|
512
515
|
is scoped to the built-in catalog, since a consumer's tours anchor on its own layer.
|
|
513
516
|
|
|
517
|
+
## Real-time store coherence: avoid the full-refresh CLOBBER
|
|
518
|
+
|
|
519
|
+
The recurring product bug behind most e2e flakes: a stale full-snapshot refresh clobbering newer
|
|
520
|
+
live state. The SPA has two delivery shapes and mixing them wrong drops live-added state with NO
|
|
521
|
+
event left to restore it.
|
|
522
|
+
|
|
523
|
+
- **Know how your entity is delivered.** A `board` event is COARSE: no payload, only a debounced
|
|
524
|
+
full `workspace.refresh()`, and `hydrate` REPLACES whole lists. A spawned task/module block
|
|
525
|
+
reaches the browser ONLY this way. Targeted events (`execution`/`bootstrap`/`initiative`) carry
|
|
526
|
+
the entity and `upsert` it, so they don't clobber. Prefer a targeted upsert for anything that
|
|
527
|
+
must appear reliably.
|
|
528
|
+
- **Full refreshes MUST be monotonic.** Two `refresh()` calls can be in flight; a staler one
|
|
529
|
+
resolving later overwrites the newer. `workspace.refresh()` guards this with a sequence. Do not
|
|
530
|
+
reintroduce an unguarded `hydrate(await fetch())`, and apply the guard to any new coalesced
|
|
531
|
+
refresh path.
|
|
532
|
+
- **Never gate readiness on a snapshot a later resync can undo.** The on-connect resync flips
|
|
533
|
+
`connected` only after it settles (which is why e2e gates on `data-connected`).
|
|
534
|
+
- **A REPLACE-style `hydrate` must never silently drop live-only state.** Either fold that state
|
|
535
|
+
into the snapshot or reconcile rather than replace.
|
|
536
|
+
- **An action's OPTIMISTIC ECHO is a clobber too, and it bypasses both guards above.** A store
|
|
537
|
+
that awaits a mutation and then assigns the returned sub-state onto the cached run
|
|
538
|
+
(`step.forkDecision`, `step.prReview`, `step.judge`, `step.followUps`) is writing straight past
|
|
539
|
+
`upsert`'s `rev` check. Where the mutation WAKES THE DRIVER, the driver's next emit routinely
|
|
540
|
+
beats the HTTP response, so the echo puts the run back; if the run then parks, nothing emits
|
|
541
|
+
again and the newer state is gone for good (the fork-chat reply that vanished, leaving a
|
|
542
|
+
"thinking…" bubble spinning). Every echo therefore goes through
|
|
543
|
+
`execution.echoAfter(executionId, send, apply)`, which captures the run's `rev` before the
|
|
544
|
+
request and drops the echo if anything advanced it. Never hand-roll the await-then-assign.
|
|
545
|
+
- **Pin it with a store-level unit test** (`stores/workspace.spec.ts` for refreshes,
|
|
546
|
+
`stores/execution.spec.ts` for echoes): drive the two orderings and assert the fresher one
|
|
547
|
+
wins.
|
|
548
|
+
|
|
549
|
+
## Internationalization (i18n) authoring
|
|
550
|
+
|
|
551
|
+
All user-facing SPA copy goes through `@nuxtjs/i18n`; never hard-code a display string. This
|
|
552
|
+
layer ships the base `en` locale, and a downstream deployment overrides by dropping its own files
|
|
553
|
+
(the per-layer deep-merge is the override seam, consumer wins key by key). Migration status:
|
|
554
|
+
[`docs/localization.md`](../../docs/localization.md).
|
|
555
|
+
|
|
556
|
+
- `i18n/locales/<locale>.json`: the catalogs (the v9+ `i18n/` convention, NOT `app/locales/`).
|
|
557
|
+
- `i18n/i18n.config.ts`: runtime vue-i18n behaviour only (fallback locale, the named
|
|
558
|
+
`numberFormats`/`datetimeFormats`). Messages are deliberately NOT here so the module can
|
|
559
|
+
deep-merge across the `extends` chain. Referenced as the BARE filename
|
|
560
|
+
`vueI18n: 'i18n.config.ts'`, never `layerDir`-anchored.
|
|
561
|
+
- `package.json` `files` MUST include `"i18n"`. Release-blocking.
|
|
562
|
+
|
|
563
|
+
**Adding a string**: add the key to `en.json` under the feature namespace, resolve with
|
|
564
|
+
`t('feature.area.key')`, and format numbers/dates through `$n`/`$d` (the named formats), never
|
|
565
|
+
raw `Intl`.
|
|
566
|
+
|
|
567
|
+
**Key conventions**: one namespace per feature; **leaf keys mirror the enum/code value verbatim**
|
|
568
|
+
so a dynamic lookup is total; **no cross-key concatenation** (a full sentence is ONE key with
|
|
569
|
+
`{named}` placeholders, plurals use the pipe form).
|
|
570
|
+
|
|
571
|
+
**Component mechanics that bite:**
|
|
572
|
+
|
|
573
|
+
- `useI18n` is auto-imported; destructure in `<script setup>` and use those fns in the template
|
|
574
|
+
so the typed-key check sees literal keys. Never `import` it.
|
|
575
|
+
- Plural + interpolation: `t(key, { vendor, count }, count)`, where the THIRD arg is the choice.
|
|
576
|
+
- **Code/format-example placeholders stay INLINE**, not in the catalog; required when they
|
|
577
|
+
contain `{`/`}` (vue-i18n metacharacters). Only prose placeholders get a key. Same for brand
|
|
578
|
+
names.
|
|
579
|
+
- **No HTML in message bodies**: drop mid-sentence `<strong>`, or use `<i18n-t>` with slots.
|
|
580
|
+
- For a vendor/enum-keyed set, build an array of STATIC literal `t()` keys, one per member.
|
|
581
|
+
Reserve the runtime-assembled key + exhaustive `Record` guard for lookups genuinely unknown
|
|
582
|
+
until runtime.
|
|
583
|
+
- Straight quotes, no em-dashes in new entries.
|
|
584
|
+
|
|
585
|
+
**Translator descriptions (`@<key>` siblings): default to NONE.** They live only in `en.json` and
|
|
586
|
+
are notes to a translator, never runtime data. Add one ONLY when a competent translator seeing
|
|
587
|
+
the English and the key path could plausibly get it wrong: homograph / part-of-speech ambiguity
|
|
588
|
+
(`@close`), proper nouns that must NOT be translated (`@kaizen`), umbrella strings hiding cases
|
|
589
|
+
the text doesn't show, placeholder/format constraints, or plural-form requirements beyond
|
|
590
|
+
English's two.
|
|
591
|
+
|
|
592
|
+
**Presenting a backend failure**: raw backend prose is DETAIL, never the description. Even with
|
|
593
|
+
no `reason` to key off, a failure is described from its STATUS CLASS through an exhaustive
|
|
594
|
+
`Record<ApiErrorCode, …>` of translated copy, and the untranslated `message` (plus a validation
|
|
595
|
+
400's `issues` and the envelope's `requestId`) is reached through a "Show details" disclosure
|
|
596
|
+
that reveals it in place. So a non-English user is never handed English as the primary
|
|
597
|
+
explanation, and the elaborate operator remedies the backend does write stay one click away
|
|
598
|
+
rather than being dropped. A new failure-presenting surface copies that split (the
|
|
599
|
+
`usePipelineErrorToast.ts` pattern; the wire vocabulary comes from `@cat-factory/contracts`).
|
|
600
|
+
|
|
601
|
+
**Drift guards** (oxlint has no `no-raw-text` rule, so these replace it):
|
|
602
|
+
|
|
603
|
+
1. **Typed message keys** make a statically written unknown `t('literal.key')` a typecheck
|
|
604
|
+
failure. This does NOT cover a runtime-assembled key.
|
|
605
|
+
2. For enum→key lookups, guard with an **exhaustive `Record<TheEnum, string>`** keyed off the
|
|
606
|
+
contracts union, plus a runtime `te()` fallback. Never rely on tier 1 alone for a
|
|
607
|
+
reason/status-keyed lookup.
|
|
608
|
+
3. `pnpm --filter @cat-factory/app run i18n:check` hard-fails on MISSING keys and reports unused
|
|
609
|
+
ones as non-blocking warnings (the catalog legitimately seeds keys ahead of use).
|
|
610
|
+
4. **Locale parity**: `i18n-locale-parity.mjs --since origin/<base>` requires a PR that adds,
|
|
611
|
+
changes, or removes an `en.json` key to make the SAME change in every other locale. It is
|
|
612
|
+
change-coupling against the merge-base, NOT full key parity.
|
|
613
|
+
|
|
614
|
+
**Translate for real: NEVER ship an English string as a non-`en` value.** The parity gate checks
|
|
615
|
+
only that the key exists, so it will pass a verbatim English copy, and that copy is a bug. The
|
|
616
|
+
only values that may legitimately match `en` are proper nouns identical across languages
|
|
617
|
+
(`DeepSeek`, `AWS Bedrock`). If you genuinely cannot produce a translation, say so in the PR
|
|
618
|
+
rather than committing a placeholder that reads as done.
|
|
619
|
+
|
|
620
|
+
Migration is incremental: when you touch a component, lift its visible copy into the catalog.
|
|
621
|
+
|
|
514
622
|
## Extending the layer (consumer modules)
|
|
515
623
|
|
|
516
624
|
A deployment can contribute its own components (result windows, nav entries, inspector
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
// architecture, or from scratch following a freeform prompt. The modal pairs the
|
|
6
6
|
// launch form with the managed base list.
|
|
7
7
|
import type { BootstrapStatus, FrameRepoType, ReferenceArchitecture } from '~/types/domain'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
8
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
9
|
+
import { appInstallationManageUrl, newRepoUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
11
10
|
|
|
12
11
|
const ui = useUiStore()
|
|
13
12
|
const bootstrap = useBootstrapStore()
|
|
@@ -137,35 +136,42 @@ watch(
|
|
|
137
136
|
{ immediate: true },
|
|
138
137
|
)
|
|
139
138
|
|
|
140
|
-
// A bootstrap run pushes into a
|
|
141
|
-
// first (the backend pre-flights the same and 409s otherwise). When the
|
|
142
|
-
// integration is on but unconnected, surface the
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
// The
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
139
|
+
// A bootstrap run pushes into a repo on the connected host, so the workspace must be
|
|
140
|
+
// connected first (the backend pre-flights the same and 409s otherwise). When the
|
|
141
|
+
// integration is on but unconnected, surface the connect prompt inline and block launch
|
|
142
|
+
// until it's bound.
|
|
143
|
+
const needsConnection = computed(() => github.available === true && !github.connected)
|
|
144
|
+
|
|
145
|
+
// The host this modal is about: the connected one, or the only one the deployment could
|
|
146
|
+
// connect while nothing is bound. Null where it offers several and none is connected, so
|
|
147
|
+
// `provider`'s own "what is connected" default can never send a GitLab deployment to github.com.
|
|
148
|
+
const hostProvider = computed(() => github.surfaceProvider)
|
|
149
|
+
const providerLabel = computed(() =>
|
|
150
|
+
hostProvider.value ? VCS_PROVIDER_LABELS[hostProvider.value] : '',
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
// The account the repo must live under — the connected account. The run pushes into an
|
|
154
|
+
// existing repo here (cat-factory doesn't create it: a GitHub App can't create repos under a
|
|
155
|
+
// personal account, and we'd rather not hold the broad Administration permission). The repo
|
|
156
|
+
// must be empty or hold only a prepopulated README/.gitignore/license — the push
|
|
157
|
+
// force-overwrites that boilerplate. The convenience link opens the host's own new-repo page,
|
|
158
|
+
// prefilled, and is ABSENT for any host `~/utils/vcs` can't name (an unresolved provider, or a
|
|
159
|
+
// GitLab whose instance nothing on the wire states); the copy and the button both key off it,
|
|
160
|
+
// so what the intro promises and what renders cannot disagree.
|
|
153
161
|
const repoOwner = computed(() => github.connection?.accountLogin ?? '')
|
|
154
|
-
const createRepoUrl = computed(() =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return `https://github.com/new?${params.toString()}`
|
|
163
|
-
})
|
|
162
|
+
const createRepoUrl = computed(() =>
|
|
163
|
+
newRepoUrl(hostProvider.value, {
|
|
164
|
+
owner: repoOwner.value,
|
|
165
|
+
name: repoName.value.trim(),
|
|
166
|
+
description: description.value.trim(),
|
|
167
|
+
private: isPrivate.value,
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
164
170
|
|
|
165
171
|
const creatingRepo = ref(false)
|
|
166
172
|
|
|
167
173
|
// The "create repository" button behaves differently per tier. Restricted orgs
|
|
168
|
-
// (the default) open
|
|
174
|
+
// (the default) open the host's new-repo page — cat-factory needs no
|
|
169
175
|
// repo-creation permission. Privileged orgs (the connection reports
|
|
170
176
|
// `canCreateRepos`) create it programmatically via the backend, with no page.
|
|
171
177
|
async function openCreateRepo() {
|
|
@@ -173,7 +179,9 @@ async function openCreateRepo() {
|
|
|
173
179
|
if (!name || repoNameError.value) return
|
|
174
180
|
|
|
175
181
|
if (!github.canCreateRepos) {
|
|
176
|
-
|
|
182
|
+
// The button is hidden without a resolved host, so there is always a URL here; the guard
|
|
183
|
+
// keeps that a local fact rather than an assumption about the template.
|
|
184
|
+
if (createRepoUrl.value) window.open(createRepoUrl.value, '_blank', 'noopener')
|
|
177
185
|
return
|
|
178
186
|
}
|
|
179
187
|
|
|
@@ -207,20 +215,15 @@ async function openCreateRepo() {
|
|
|
207
215
|
// "not accessible to the GitHub App". Link straight to the connected
|
|
208
216
|
// installation's settings page, where the user adds the repo to its access list
|
|
209
217
|
// in one click — no install/connect round-trip (the workspace is already bound).
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!conn) return undefined
|
|
213
|
-
return conn.targetType === 'Organization'
|
|
214
|
-
? `https://github.com/organizations/${conn.accountLogin}/settings/installations/${conn.installationId}`
|
|
215
|
-
: `https://github.com/settings/installations/${conn.installationId}`
|
|
216
|
-
})
|
|
218
|
+
// Absent on a PAT connection, which grants no per-installation access (see `~/utils/vcs`).
|
|
219
|
+
const manageInstallUrl = computed(() => appInstallationManageUrl(github.connection))
|
|
217
220
|
|
|
218
221
|
function openManageInstall() {
|
|
219
222
|
if (manageInstallUrl.value) window.open(manageInstallUrl.value, '_blank', 'noopener')
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
const canLaunch = computed(() => {
|
|
223
|
-
if (
|
|
226
|
+
if (needsConnection.value) return false
|
|
224
227
|
if (!repoName.value.trim() || repoNameError.value) return false
|
|
225
228
|
return usingReference.value ? !!selectedArchId.value : instructions.value.trim().length > 0
|
|
226
229
|
})
|
|
@@ -420,22 +423,34 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
420
423
|
<UModal v-model:open="open" :title="t('bootstrap.title')" :ui="{ content: 'max-w-2xl' }">
|
|
421
424
|
<template #body>
|
|
422
425
|
<div class="space-y-6">
|
|
426
|
+
<!-- Three states, because each promises the user something different about the repo.
|
|
427
|
+
cat-factory creates it (privileged App tier, so a provider is always resolved);
|
|
428
|
+
the user creates it in one click on a host we can name; or the user creates it
|
|
429
|
+
themselves somewhere we cannot name, where promising a click below would be a lie
|
|
430
|
+
(the button is absent for exactly the same reason). -->
|
|
423
431
|
<p class="text-sm text-slate-400">
|
|
424
|
-
{{
|
|
432
|
+
{{
|
|
433
|
+
github.canCreateRepos
|
|
434
|
+
? t('vcs.bootstrap.introCanCreate', { provider: providerLabel })
|
|
435
|
+
: createRepoUrl
|
|
436
|
+
? t('vcs.bootstrap.introManual', { provider: providerLabel })
|
|
437
|
+
: t('vcs.bootstrap.introManualAny')
|
|
438
|
+
}}
|
|
425
439
|
</p>
|
|
426
440
|
|
|
427
|
-
<!-- not connected: a run
|
|
441
|
+
<!-- not connected: a run pushes to the host, so connect before launching. Offer
|
|
442
|
+
whichever methods the deployment serves, never just the GitHub App. -->
|
|
428
443
|
<div
|
|
429
|
-
v-if="
|
|
444
|
+
v-if="needsConnection"
|
|
430
445
|
class="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-3"
|
|
431
446
|
>
|
|
432
447
|
<div class="flex items-start gap-2">
|
|
433
448
|
<UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
|
|
434
449
|
<p class="text-sm text-amber-200/90">
|
|
435
|
-
{{ t('bootstrap.
|
|
450
|
+
{{ t('vcs.bootstrap.connectPrompt') }}
|
|
436
451
|
</p>
|
|
437
452
|
</div>
|
|
438
|
-
<
|
|
453
|
+
<VcsConnectSurfaces />
|
|
439
454
|
</div>
|
|
440
455
|
|
|
441
456
|
<!-- launch -->
|
|
@@ -484,7 +499,11 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
484
499
|
:placeholder="t('bootstrap.targetRepo.namePlaceholder')"
|
|
485
500
|
class="w-full"
|
|
486
501
|
/>
|
|
502
|
+
<!-- Creating the repo for the user needs no host name; sending them to the
|
|
503
|
+
host's own form needs one, so that variant waits until a host is
|
|
504
|
+
resolved rather than guessing which page to open. -->
|
|
487
505
|
<UButton
|
|
506
|
+
v-if="github.canCreateRepos || createRepoUrl"
|
|
488
507
|
color="neutral"
|
|
489
508
|
variant="subtle"
|
|
490
509
|
:icon="github.canCreateRepos ? 'i-lucide-plus' : 'i-lucide-external-link'"
|
|
@@ -493,14 +512,14 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
493
512
|
:title="
|
|
494
513
|
github.canCreateRepos
|
|
495
514
|
? t('bootstrap.createRepo.titleNow')
|
|
496
|
-
: t('bootstrap.
|
|
515
|
+
: t('vcs.bootstrap.createRepoTitle', { provider: providerLabel })
|
|
497
516
|
"
|
|
498
517
|
@click="openCreateRepo"
|
|
499
518
|
>
|
|
500
519
|
{{
|
|
501
520
|
github.canCreateRepos
|
|
502
521
|
? t('bootstrap.createRepo.now')
|
|
503
|
-
: t('bootstrap.
|
|
522
|
+
: t('vcs.bootstrap.createRepoOn', { provider: providerLabel })
|
|
504
523
|
}}
|
|
505
524
|
</UButton>
|
|
506
525
|
</div>
|
|
@@ -671,9 +690,11 @@ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
|
|
|
671
690
|
v-if="showArchForm"
|
|
672
691
|
class="space-y-3 rounded-md border border-slate-700 bg-slate-900/80 p-3"
|
|
673
692
|
>
|
|
693
|
+
<!-- The options come from the connected projection, so a repo to pick means a
|
|
694
|
+
connection exists and `providerLabel` names it rather than guessing. -->
|
|
674
695
|
<UFormField
|
|
675
696
|
v-if="hasRepoOptions"
|
|
676
|
-
:label="t('bootstrap.
|
|
697
|
+
:label="t('vcs.bootstrap.archPickRepo', { provider: providerLabel })"
|
|
677
698
|
:description="t('bootstrap.arch.pickRepo.description')"
|
|
678
699
|
>
|
|
679
700
|
<USelect
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Add a board service backed by an EXISTING
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// Add a board service backed by an EXISTING repository — no bootstrap run. Unlike the
|
|
3
|
+
// bootstrap modal (which creates a repo and has an agent adapt it in a container), this
|
|
4
|
+
// just links a repo the workspace's connection can reach to a fresh, `ready` service
|
|
5
|
+
// frame. The workspace need not track the repo yet: the backend links + syncs it on
|
|
6
|
+
// import. On a GitHub App connection, a repo the App can't see yet is granted from here
|
|
7
|
+
// and searched for again; a PAT connection has no such page (see `~/utils/vcs`), so what
|
|
8
|
+
// is listed follows the token's own access.
|
|
8
9
|
//
|
|
9
10
|
// MONOREPO support: a repo flagged a monorepo can back SEVERAL services, each
|
|
10
11
|
// pinned to a subdirectory. When the selected repo is a monorepo, the user
|
|
@@ -12,11 +13,12 @@
|
|
|
12
13
|
// parent folder, in one pass — then adds them all at once. Directories that
|
|
13
14
|
// already back a service on this board are shown but not selectable.
|
|
14
15
|
import type { FrameRepoType, GitHubAvailableRepo } from '~/types/domain'
|
|
15
|
-
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
16
16
|
import RepoSearchEmpty from '~/components/github/RepoSearchEmpty.vue'
|
|
17
17
|
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
18
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
18
19
|
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
19
20
|
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
21
|
+
import { appInstallationManageUrl, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
20
22
|
|
|
21
23
|
const { t } = useI18n()
|
|
22
24
|
|
|
@@ -64,7 +66,22 @@ watch(
|
|
|
64
66
|
)
|
|
65
67
|
|
|
66
68
|
// The integration is on but this workspace isn't bound yet — connect first.
|
|
67
|
-
const
|
|
69
|
+
const needsConnection = computed(() => github.available === true && !github.connected)
|
|
70
|
+
|
|
71
|
+
// Brand name of whatever the workspace connected, for the hint that names it. Only read where
|
|
72
|
+
// a connection exists (the hints below the picker), so `provider` is the right question there.
|
|
73
|
+
const providerLabel = computed(() => VCS_PROVIDER_LABELS[github.provider])
|
|
74
|
+
|
|
75
|
+
// Which remedy the picker's hint offers: an App installation sends the user to its repo-access
|
|
76
|
+
// list, a pasted token to the token's own scope. Asked of the CONNECTION rather than of the
|
|
77
|
+
// manage URL below, so a host whose settings page we could not build (an Enterprise install,
|
|
78
|
+
// say) never tells an App-connected user to go check their token's scope.
|
|
79
|
+
const isAppConnection = computed(() => github.connection?.method === 'app')
|
|
80
|
+
|
|
81
|
+
// The intro renders BEFORE a connection may exist, so it asks `surfaceProvider` instead and
|
|
82
|
+
// stays neutral where the deployment offers several and none is bound: naming one would be a
|
|
83
|
+
// guess, and `provider`'s own default would name GitHub on a GitLab-only deployment.
|
|
84
|
+
const introProvider = computed(() => github.surfaceProvider)
|
|
68
85
|
|
|
69
86
|
// Repos whose service is ALREADY mounted on THIS board can't be added again — adding here would
|
|
70
87
|
// be a no-op. A repo whose service lives on ANOTHER board in the org stays addable: adding it
|
|
@@ -201,15 +218,10 @@ function clearSelection() {
|
|
|
201
218
|
resetSelection()
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
// The App's installation settings page — where the user grants it access to a
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!conn) return undefined
|
|
209
|
-
return conn.targetType === 'Organization'
|
|
210
|
-
? `https://github.com/organizations/${conn.accountLogin}/settings/installations/${conn.installationId}`
|
|
211
|
-
: `https://github.com/settings/installations/${conn.installationId}`
|
|
212
|
-
})
|
|
221
|
+
// The App's installation settings page — where the user grants it access to a repo it can't
|
|
222
|
+
// see yet (mirrors the bootstrap modal's "grant access" link). Absent on a PAT connection,
|
|
223
|
+
// which has no installation to manage, so the affordance and its hint both drop out.
|
|
224
|
+
const manageInstallUrl = computed(() => appInstallationManageUrl(github.connection))
|
|
213
225
|
|
|
214
226
|
function openManageInstall() {
|
|
215
227
|
if (manageInstallUrl.value) window.open(manageInstallUrl.value, '_blank', 'noopener')
|
|
@@ -242,14 +254,14 @@ watch(
|
|
|
242
254
|
// multi-selects directories and adds them together via `addServices`.
|
|
243
255
|
const canAdd = computed(
|
|
244
256
|
() =>
|
|
245
|
-
!
|
|
257
|
+
!needsConnection.value &&
|
|
246
258
|
selectedRepoId.value !== undefined &&
|
|
247
259
|
!isMonorepo.value &&
|
|
248
260
|
!configuredBlockId.value,
|
|
249
261
|
)
|
|
250
262
|
const canAddServices = computed(
|
|
251
263
|
() =>
|
|
252
|
-
!
|
|
264
|
+
!needsConnection.value &&
|
|
253
265
|
selectedRepoId.value !== undefined &&
|
|
254
266
|
isMonorepo.value &&
|
|
255
267
|
selectedDirectories.value.length > 0,
|
|
@@ -357,27 +369,36 @@ function done() {
|
|
|
357
369
|
<template #body>
|
|
358
370
|
<div class="space-y-6">
|
|
359
371
|
<p class="text-sm text-slate-400">
|
|
360
|
-
{{
|
|
372
|
+
{{
|
|
373
|
+
introProvider
|
|
374
|
+
? t('vcs.addService.intro', { provider: VCS_PROVIDER_LABELS[introProvider] })
|
|
375
|
+
: t('vcs.addService.introAny')
|
|
376
|
+
}}
|
|
361
377
|
</p>
|
|
362
378
|
|
|
363
|
-
<!-- not connected: linking a repo needs
|
|
379
|
+
<!-- not connected: linking a repo needs a connection bound to this workspace, so
|
|
380
|
+
offer whichever connect methods the deployment serves (never just the App) -->
|
|
364
381
|
<div
|
|
365
|
-
v-if="
|
|
382
|
+
v-if="needsConnection"
|
|
366
383
|
class="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-3"
|
|
367
384
|
>
|
|
368
385
|
<div class="flex items-start gap-2">
|
|
369
386
|
<UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
|
|
370
387
|
<p class="text-sm text-amber-200/90">
|
|
371
|
-
{{ t('
|
|
388
|
+
{{ t('vcs.addService.connectFirst') }}
|
|
372
389
|
</p>
|
|
373
390
|
</div>
|
|
374
|
-
<
|
|
391
|
+
<VcsConnectSurfaces />
|
|
375
392
|
</div>
|
|
376
393
|
|
|
377
394
|
<template v-else>
|
|
378
395
|
<UFormField
|
|
379
396
|
:label="t('github.addService.repository')"
|
|
380
|
-
:description="
|
|
397
|
+
:description="
|
|
398
|
+
isAppConnection
|
|
399
|
+
? t('vcs.addService.repositoryHintApp')
|
|
400
|
+
: t('vcs.addService.repositoryHintToken', { provider: providerLabel })
|
|
401
|
+
"
|
|
381
402
|
required
|
|
382
403
|
>
|
|
383
404
|
<!-- The wrapper, not the UInputMenu itself, carries the anchor: a tutorial tour
|
|
@@ -521,9 +542,10 @@ function done() {
|
|
|
521
542
|
<ServiceFragments :block="configuredBlock" default-open />
|
|
522
543
|
</div>
|
|
523
544
|
|
|
524
|
-
|
|
545
|
+
<!-- App connections only: a pasted token has no installation whose repo access
|
|
546
|
+
could be edited, so there is no page to send the user to. -->
|
|
547
|
+
<div v-if="manageInstallUrl" class="flex flex-wrap items-center gap-2">
|
|
525
548
|
<UButton
|
|
526
|
-
v-if="manageInstallUrl"
|
|
527
549
|
color="neutral"
|
|
528
550
|
variant="subtle"
|
|
529
551
|
size="sm"
|
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
// Hard onboarding gate shown after login when the VCS integration is enabled but the workspace
|
|
3
3
|
// has no connection yet. cat-factory's whole flow runs on a connected repository host (agents
|
|
4
4
|
// open pull/merge requests on the user's repos), so the board is withheld until the workspace
|
|
5
|
-
// connects one.
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
// pick-an-existing-installation path, and <GitLabConnect> takes a pasted GitLab PAT. A "Sign
|
|
9
|
-
// out" escape hatch avoids trapping a user who needs to switch accounts.
|
|
10
|
-
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
11
|
-
import GitLabConnect from '~/components/vcs/GitLabConnect.vue'
|
|
5
|
+
// connects one. <VcsConnectSurfaces> renders whichever connect methods the deployment serves; a
|
|
6
|
+
// "Sign out" escape hatch avoids trapping a user who needs to switch accounts.
|
|
7
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
12
8
|
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
13
9
|
|
|
14
10
|
const { t } = useI18n()
|
|
@@ -41,22 +37,7 @@ const title = computed(() =>
|
|
|
41
37
|
</p>
|
|
42
38
|
</div>
|
|
43
39
|
|
|
44
|
-
<
|
|
45
|
-
<p class="mb-3 text-sm text-slate-400">{{ t('github.onboarding.appIntro') }}</p>
|
|
46
|
-
<GitHubConnect />
|
|
47
|
-
</template>
|
|
48
|
-
<USeparator
|
|
49
|
-
v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
|
|
50
|
-
class="my-4"
|
|
51
|
-
:label="t('vcs.connect.or')"
|
|
52
|
-
/>
|
|
53
|
-
<GitLabConnect v-if="github.canConnectGitLabPat" />
|
|
54
|
-
<p
|
|
55
|
-
v-if="!github.canConnectGitHubApp && !github.canConnectGitLabPat"
|
|
56
|
-
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
57
|
-
>
|
|
58
|
-
{{ t('vcs.connect.noneConfigured') }}
|
|
59
|
-
</p>
|
|
40
|
+
<VcsConnectSurfaces :app-intro="t('github.onboarding.appIntro')" />
|
|
60
41
|
|
|
61
42
|
<p
|
|
62
43
|
v-if="auth.required && auth.user"
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// comment) go straight to the repo via the backend's installation token.
|
|
2
|
+
// Source-control panel: connect the workspace, manage the connection (disconnect / resync),
|
|
3
|
+
// and browse the projected repos, branches, pull requests and issues the backend caches.
|
|
4
|
+
// Mirrors the document-source connect/import surface. Writes (new branch, open/merge PR,
|
|
5
|
+
// comment) go straight to the repo via the backend's connection credential.
|
|
7
6
|
import type { GitHubPullRequest, GitHubRepo, VcsProvider } from '~/types/domain'
|
|
8
|
-
// Explicit
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the tag unambiguously.
|
|
12
|
-
import GitHubConnect from './GitHubConnect.vue'
|
|
7
|
+
// Explicit imports: the auto-import name for a component nested under a like-named directory
|
|
8
|
+
// (github/BranchProtectionPreflight) doesn't match the tag used below, so it would silently
|
|
9
|
+
// render as an empty element. Importing by path binds each tag unambiguously.
|
|
13
10
|
import BranchProtectionPreflight from './BranchProtectionPreflight.vue'
|
|
14
|
-
import
|
|
11
|
+
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
15
12
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
16
13
|
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
17
14
|
|
|
@@ -33,9 +30,7 @@ const back = useIntegrationBack(open)
|
|
|
33
30
|
// Provider-aware chrome: once connected, the panel is titled for the provider the workspace
|
|
34
31
|
// actually connected; before that it names the sole connectable provider, or stays neutral when
|
|
35
32
|
// the deployment serves several. Brand names are verbatim in every locale (see `~/utils/vcs`).
|
|
36
|
-
const chromeProvider = computed(() =>
|
|
37
|
-
github.connected ? github.provider : github.soleConnectProvider,
|
|
38
|
-
)
|
|
33
|
+
const chromeProvider = computed(() => github.surfaceProvider)
|
|
39
34
|
const panelTitle = computed(() =>
|
|
40
35
|
chromeProvider.value ? VCS_PROVIDER_LABELS[chromeProvider.value] : t('vcs.panel.title'),
|
|
41
36
|
)
|
|
@@ -283,26 +278,7 @@ async function merge(pr: GitHubPullRequest) {
|
|
|
283
278
|
<template #body>
|
|
284
279
|
<div class="space-y-5">
|
|
285
280
|
<!-- not connected: connect -->
|
|
286
|
-
<
|
|
287
|
-
<!-- One connect surface per method the deployment actually serves: the App
|
|
288
|
-
installation picker only where a GitHub App is configured, the PAT box only
|
|
289
|
-
where the per-workspace GitLab connect is wired. -->
|
|
290
|
-
<template v-if="github.canConnectGitHubApp">
|
|
291
|
-
<p class="text-sm text-slate-400">{{ t('github.panel.connectIntro') }}</p>
|
|
292
|
-
<GitHubConnect />
|
|
293
|
-
</template>
|
|
294
|
-
<USeparator
|
|
295
|
-
v-if="github.canConnectGitHubApp && github.canConnectGitLabPat"
|
|
296
|
-
:label="t('vcs.connect.or')"
|
|
297
|
-
/>
|
|
298
|
-
<GitLabConnect v-if="github.canConnectGitLabPat" />
|
|
299
|
-
<p
|
|
300
|
-
v-if="!github.canConnectGitHubApp && !github.canConnectGitLabPat"
|
|
301
|
-
class="rounded-md border border-dashed border-slate-800 px-3 py-3 text-sm text-slate-400"
|
|
302
|
-
>
|
|
303
|
-
{{ t('vcs.connect.noneConfigured') }}
|
|
304
|
-
</p>
|
|
305
|
-
</template>
|
|
281
|
+
<VcsConnectSurfaces v-if="!github.connected" :app-intro="t('github.panel.connectIntro')" />
|
|
306
282
|
|
|
307
283
|
<!-- connected: manage + browse -->
|
|
308
284
|
<template v-else>
|