@cat-factory/app 0.93.0 → 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/github/AddServiceFromRepoModal.vue +26 -35
- package/app/components/github/RepoSearchEmpty.vue +18 -0
- package/app/components/panels/inspector/DocReferenceRepos.vue +144 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +3 -0
- package/app/components/settings/InfrastructureWindow.vue +18 -2
- package/app/components/settings/SharedStacksPanel.vue +363 -0
- package/app/composables/api/sharedStacks.ts +42 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/useRepoSearch.ts +66 -0
- package/app/stores/github.ts +13 -0
- package/app/stores/sharedStacks.ts +65 -0
- package/app/stores/workspace.ts +2 -0
- package/app/types/domain.ts +1 -0
- package/app/types/sharedStacks.ts +8 -0
- package/i18n/locales/en.json +52 -0
- package/i18n/locales/es.json +52 -0
- package/i18n/locales/fr.json +52 -0
- package/i18n/locales/he.json +52 -0
- package/i18n/locales/ja.json +52 -0
- package/i18n/locales/pl.json +52 -0
- package/i18n/locales/tr.json +52 -0
- package/i18n/locales/uk.json +52 -0
- package/package.json +2 -2
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// pinned to a subdirectory. When the selected repo is a monorepo, the user
|
|
11
11
|
// browses its tree and picks the service's directory before adding (and may add
|
|
12
12
|
// more than one, a subset of the repo's services).
|
|
13
|
-
import { refDebounced } from '@vueuse/core'
|
|
14
13
|
import type { FrameRepoType, GitHubAvailableRepo } from '~/types/domain'
|
|
15
14
|
import GitHubConnect from '~/components/github/GitHubConnect.vue'
|
|
15
|
+
import RepoSearchEmpty from '~/components/github/RepoSearchEmpty.vue'
|
|
16
16
|
import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
17
17
|
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
18
18
|
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
@@ -89,33 +89,27 @@ function toRepoItem(r: GitHubAvailableRepo) {
|
|
|
89
89
|
return { label: `${r.owner}/${r.name}${suffix}`, value: r.githubId, disabled: onBoard }
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
// Server-side, debounced, min-length-gated repo search (a wide App install / PAT can expose
|
|
93
|
+
// hundreds of repos, too many to prefetch and filter in the browser). Shared with the doc-task
|
|
94
|
+
// reference-repo picker via `useRepoSearch`; `repoResults` is this picker's own result list, so
|
|
95
|
+
// the two never clobber each other. Nothing is fetched on open — the field prompts for more
|
|
96
|
+
// characters below the gate; granting the App access needs no manual refresh (the next search
|
|
97
|
+
// hits GitHub live).
|
|
98
|
+
const {
|
|
99
|
+
search: repoSearch,
|
|
100
|
+
query: repoQueryRaw,
|
|
101
|
+
belowMinChars,
|
|
102
|
+
results: repoResults,
|
|
103
|
+
loading: repoLoading,
|
|
104
|
+
reset: resetRepoSearch,
|
|
105
|
+
} = useRepoSearch()
|
|
106
|
+
|
|
107
|
+
const repoItems = computed(() => repoResults.value.map(toRepoItem))
|
|
107
108
|
|
|
108
109
|
// The selected repo, captured when picked (below). The loaded list is volatile — a later
|
|
109
|
-
// search replaces it — so the selection can't be derived from
|
|
110
|
+
// search replaces it — so the selection can't be derived from the results after the fact.
|
|
110
111
|
const selectedRepo = ref<GitHubAvailableRepo | undefined>(undefined)
|
|
111
112
|
|
|
112
|
-
// Fetch matches server-side as the debounced query changes; below the gate clear the list
|
|
113
|
-
// so stale matches don't linger (the hint shows instead). Granting the App access needs no
|
|
114
|
-
// manual refresh — the next search hits GitHub live, with no cached list to invalidate.
|
|
115
|
-
watch(repoQueryRaw, (q) => {
|
|
116
|
-
void github.loadAvailableRepos(q.length >= MIN_SEARCH_LEN ? q : '')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
113
|
// The server already filtered, so the matches ARE the loaded repos (empty below the gate).
|
|
120
114
|
const queryMatches = computed(() => (belowMinChars.value ? [] : repoItems.value))
|
|
121
115
|
|
|
@@ -148,7 +142,7 @@ function toggleMonorepo(value: boolean) {
|
|
|
148
142
|
watch(selectedRepoId, (id) => {
|
|
149
143
|
if (id === undefined) selectedRepo.value = undefined
|
|
150
144
|
else {
|
|
151
|
-
const found =
|
|
145
|
+
const found = repoResults.value.find((r) => r.githubId === id)
|
|
152
146
|
if (found) selectedRepo.value = found
|
|
153
147
|
}
|
|
154
148
|
isMonorepo.value = selectedRepo.value?.isMonorepo === true
|
|
@@ -161,7 +155,7 @@ function resetSelection() {
|
|
|
161
155
|
selectedDirectory.value = undefined
|
|
162
156
|
isMonorepo.value = false
|
|
163
157
|
configuredBlockId.value = undefined
|
|
164
|
-
|
|
158
|
+
resetRepoSearch()
|
|
165
159
|
selectedType.value = 'service'
|
|
166
160
|
}
|
|
167
161
|
|
|
@@ -296,7 +290,7 @@ function done() {
|
|
|
296
290
|
:items="repoMenuItems"
|
|
297
291
|
:ignore-filter="true"
|
|
298
292
|
value-key="value"
|
|
299
|
-
:loading="
|
|
293
|
+
:loading="repoLoading"
|
|
300
294
|
icon="i-lucide-search"
|
|
301
295
|
:placeholder="t('github.addService.searchPlaceholder')"
|
|
302
296
|
class="w-full"
|
|
@@ -312,14 +306,11 @@ function done() {
|
|
|
312
306
|
/>
|
|
313
307
|
</template>
|
|
314
308
|
<template #empty>
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<span v-else-if="!github.loadingAvailable">{{
|
|
321
|
-
t('github.addService.noMatches', { query: repoQueryRaw })
|
|
322
|
-
}}</span>
|
|
309
|
+
<RepoSearchEmpty
|
|
310
|
+
:below-min-chars="belowMinChars"
|
|
311
|
+
:loading="repoLoading"
|
|
312
|
+
:query="repoQueryRaw"
|
|
313
|
+
/>
|
|
323
314
|
</template>
|
|
324
315
|
</UInputMenu>
|
|
325
316
|
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The empty-state body shared by the GitHub repo pickers (the add-service modal and the doc-task
|
|
3
|
+
// reference-repo picker), both driven by `useRepoSearch`: below the min-length gate it prompts for
|
|
4
|
+
// more characters; otherwise (once a search has settled) it reports no matches. Kept as one
|
|
5
|
+
// component so the two pickers can't drift on the empty-state copy or behaviour.
|
|
6
|
+
import { REPO_SEARCH_MIN_LEN } from '~/composables/useRepoSearch'
|
|
7
|
+
|
|
8
|
+
defineProps<{ belowMinChars: boolean; loading: boolean; query: string }>()
|
|
9
|
+
|
|
10
|
+
const { t } = useI18n()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span v-if="belowMinChars">
|
|
15
|
+
{{ t('github.addService.searchMinChars', { min: REPO_SEARCH_MIN_LEN }, REPO_SEARCH_MIN_LEN) }}
|
|
16
|
+
</span>
|
|
17
|
+
<span v-else-if="!loading">{{ t('github.addService.noMatches', { query }) }}</span>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Reference repositories for a document-authoring task. The doc-writer agent clones each attached
|
|
3
|
+
// repo READ-ONLY as a sibling checkout it may read (to reuse existing solutions as a reference)
|
|
4
|
+
// while drafting — it never writes to them. Any repo the workspace's GitHub App (or the signed-in
|
|
5
|
+
// user's PAT) can reach may be attached, so this reuses the SAME server-side, debounced repo
|
|
6
|
+
// search as the add-service picker (`useRepoSearch`), not a filter over the synced projection.
|
|
7
|
+
import type { Block, GitHubAvailableRepo, ReferenceRepo } from '~/types/domain'
|
|
8
|
+
import RepoSearchEmpty from '~/components/github/RepoSearchEmpty.vue'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{ block: Block }>()
|
|
11
|
+
|
|
12
|
+
const { t } = useI18n()
|
|
13
|
+
const github = useGitHubStore()
|
|
14
|
+
const board = useBoardStore()
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
search: repoSearch,
|
|
18
|
+
query: repoQuery,
|
|
19
|
+
belowMinChars,
|
|
20
|
+
results: repoResults,
|
|
21
|
+
loading: repoLoading,
|
|
22
|
+
reset: resetRepoSearch,
|
|
23
|
+
} = useRepoSearch()
|
|
24
|
+
|
|
25
|
+
const attached = computed<ReferenceRepo[]>(() => props.block.referenceRepos ?? [])
|
|
26
|
+
// Keyed by the provider-neutral repo id. The search results are `GitHubAvailableRepo`s (whose id
|
|
27
|
+
// field is `githubId`), so a menu item's `githubId` is compared against this set of `repoId`s —
|
|
28
|
+
// the same numeric id space on every provider.
|
|
29
|
+
const attachedIds = computed(() => new Set(attached.value.map((r) => r.repoId)))
|
|
30
|
+
|
|
31
|
+
// Menu items: the searched repos, an already-attached one disabled so it can't be added twice.
|
|
32
|
+
const repoItems = computed(() =>
|
|
33
|
+
belowMinChars.value
|
|
34
|
+
? []
|
|
35
|
+
: repoResults.value.map((r) => ({
|
|
36
|
+
label: `${r.owner}/${r.name}${r.personal ? t('github.addService.repoLabel.personal') : ''}`,
|
|
37
|
+
value: r.githubId,
|
|
38
|
+
disabled: attachedIds.value.has(r.githubId),
|
|
39
|
+
})),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// The picked repo id; watched to attach then clear (the combobox has no "add" affordance of its
|
|
43
|
+
// own, so selecting a repo IS the attach action).
|
|
44
|
+
const pickedId = ref<number | undefined>(undefined)
|
|
45
|
+
|
|
46
|
+
watch(pickedId, (id) => {
|
|
47
|
+
if (id === undefined) return
|
|
48
|
+
const repo = repoResults.value.find((r) => r.githubId === id)
|
|
49
|
+
pickedId.value = undefined
|
|
50
|
+
if (!repo || attachedIds.value.has(id)) return
|
|
51
|
+
attach(repo)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
function toReference(repo: GitHubAvailableRepo): ReferenceRepo {
|
|
55
|
+
return {
|
|
56
|
+
repoId: repo.githubId,
|
|
57
|
+
owner: repo.owner,
|
|
58
|
+
name: repo.name,
|
|
59
|
+
// A repo with no reported default branch is rare; fall back to `main` so the clone has a ref.
|
|
60
|
+
defaultBranch: repo.defaultBranch ?? 'main',
|
|
61
|
+
// A repo the workspace connection reaches carries that connection; a PAT-only (`personal`) repo
|
|
62
|
+
// has none, so the run clones it with the initiator's own token instead.
|
|
63
|
+
...(repo.personal || !github.connection
|
|
64
|
+
? {}
|
|
65
|
+
: { connectionId: github.connection.installationId }),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function attach(repo: GitHubAvailableRepo) {
|
|
70
|
+
board.updateBlock(props.block.id, {
|
|
71
|
+
referenceRepos: [...attached.value, toReference(repo)],
|
|
72
|
+
})
|
|
73
|
+
resetRepoSearch()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function detach(repoId: number) {
|
|
77
|
+
board.updateBlock(props.block.id, {
|
|
78
|
+
referenceRepos: attached.value.filter((r) => r.repoId !== repoId),
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<div data-testid="doc-reference-repos">
|
|
85
|
+
<div class="mb-1 flex items-center justify-between">
|
|
86
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
87
|
+
{{ t('inspector.referenceRepos.title') }}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Attached reference repos: chips with a remove control. -->
|
|
92
|
+
<div v-if="attached.length" class="mb-1.5 flex flex-wrap gap-1">
|
|
93
|
+
<UBadge
|
|
94
|
+
v-for="r in attached"
|
|
95
|
+
:key="r.repoId"
|
|
96
|
+
size="sm"
|
|
97
|
+
variant="soft"
|
|
98
|
+
color="neutral"
|
|
99
|
+
data-testid="reference-repo-chip"
|
|
100
|
+
>
|
|
101
|
+
{{ r.owner }}/{{ r.name }}
|
|
102
|
+
<UButton
|
|
103
|
+
color="neutral"
|
|
104
|
+
variant="link"
|
|
105
|
+
size="xs"
|
|
106
|
+
icon="i-lucide-x"
|
|
107
|
+
:aria-label="t('inspector.referenceRepos.remove', { repo: `${r.owner}/${r.name}` })"
|
|
108
|
+
data-testid="reference-repo-remove"
|
|
109
|
+
@click="detach(r.repoId)"
|
|
110
|
+
/>
|
|
111
|
+
</UBadge>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- The picker: only usable once the workspace's GitHub App is connected. -->
|
|
115
|
+
<UInputMenu
|
|
116
|
+
v-if="github.connected"
|
|
117
|
+
v-model="pickedId"
|
|
118
|
+
v-model:search-term="repoSearch"
|
|
119
|
+
:items="repoItems"
|
|
120
|
+
:ignore-filter="true"
|
|
121
|
+
value-key="value"
|
|
122
|
+
:loading="repoLoading"
|
|
123
|
+
icon="i-lucide-search"
|
|
124
|
+
:placeholder="t('github.addService.searchPlaceholder')"
|
|
125
|
+
class="w-full"
|
|
126
|
+
data-testid="reference-repo-search"
|
|
127
|
+
>
|
|
128
|
+
<template #empty>
|
|
129
|
+
<RepoSearchEmpty
|
|
130
|
+
:below-min-chars="belowMinChars"
|
|
131
|
+
:loading="repoLoading"
|
|
132
|
+
:query="repoQuery"
|
|
133
|
+
/>
|
|
134
|
+
</template>
|
|
135
|
+
</UInputMenu>
|
|
136
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
137
|
+
{{ t('inspector.referenceRepos.connectFirst') }}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="mt-1 text-[11px] text-slate-500">
|
|
141
|
+
{{ t('inspector.referenceRepos.hint') }}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
@@ -480,6 +480,9 @@ const technicalLabel = computed(() => {
|
|
|
480
480
|
</div>
|
|
481
481
|
</div>
|
|
482
482
|
|
|
483
|
+
<!-- reference repositories: read-only repos the doc-writer reads while drafting (doc tasks) -->
|
|
484
|
+
<DocReferenceRepos v-if="block.taskType === 'document'" :block="block" />
|
|
485
|
+
|
|
483
486
|
<!-- issue-tracker writeback overrides -->
|
|
484
487
|
<div>
|
|
485
488
|
<div class="mb-1 flex items-center justify-between">
|
|
@@ -14,6 +14,10 @@ import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
|
14
14
|
import InfrastructureBackendPicker from '~/components/settings/InfrastructureBackendPicker.vue'
|
|
15
15
|
import InfraHandlersConfigurator from '~/components/settings/InfraHandlersConfigurator.vue'
|
|
16
16
|
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
17
|
+
import SharedStacksPanel from '~/components/settings/SharedStacksPanel.vue'
|
|
18
|
+
|
|
19
|
+
// The shared-stacks tab uses its own slot key beyond the provider-connection kinds.
|
|
20
|
+
type InfraTabValue = ProviderConnectionKind | 'shared-stacks'
|
|
17
21
|
|
|
18
22
|
const { t } = useI18n()
|
|
19
23
|
const ui = useUiStore()
|
|
@@ -37,7 +41,7 @@ const agentsAvailable = computed(() => (auth.infrastructure?.execution.available
|
|
|
37
41
|
const envsAvailable = computed(() => (auth.infrastructure?.testEnv.available.length ?? 0) > 0)
|
|
38
42
|
|
|
39
43
|
const tabs = computed(() => {
|
|
40
|
-
const out: { value:
|
|
44
|
+
const out: { value: InfraTabValue; label: string; icon: string; slot: string }[] = []
|
|
41
45
|
if (agentsAvailable.value)
|
|
42
46
|
out.push({
|
|
43
47
|
value: 'runner-pool',
|
|
@@ -52,10 +56,19 @@ const tabs = computed(() => {
|
|
|
52
56
|
icon: 'i-lucide-cloud',
|
|
53
57
|
slot: 'environment',
|
|
54
58
|
})
|
|
59
|
+
// Shared stacks are long-lived compose infra a Tester environment attaches to, so they live
|
|
60
|
+
// alongside the test-environment config (shown wherever an environment backend is available).
|
|
61
|
+
if (envsAvailable.value)
|
|
62
|
+
out.push({
|
|
63
|
+
value: 'shared-stacks',
|
|
64
|
+
label: t('settings.sharedStacks.tab'),
|
|
65
|
+
icon: 'i-lucide-layers',
|
|
66
|
+
slot: 'shared-stacks',
|
|
67
|
+
})
|
|
55
68
|
return out
|
|
56
69
|
})
|
|
57
70
|
|
|
58
|
-
const activeTab = ref<
|
|
71
|
+
const activeTab = ref<InfraTabValue>(ui.infrastructureTab)
|
|
59
72
|
|
|
60
73
|
// Honour the deep-linked tab each time the window opens, falling back to the first available
|
|
61
74
|
// tab if the requested one is off.
|
|
@@ -124,6 +137,9 @@ watch([tabs, () => store.loaded], () => {
|
|
|
124
137
|
<InfraHandlersConfigurator />
|
|
125
138
|
</div>
|
|
126
139
|
</template>
|
|
140
|
+
<template #shared-stacks>
|
|
141
|
+
<SharedStacksPanel />
|
|
142
|
+
</template>
|
|
127
143
|
</UTabs>
|
|
128
144
|
|
|
129
145
|
<p v-else class="px-1 py-6 text-center text-sm text-slate-500">
|