@cat-factory/app 0.182.0 → 0.183.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 +16 -3
- package/app/components/board/nodes/BlockNode.vue +14 -4
- package/app/components/board/nodes/InitiativeCard.vue +27 -2
- package/app/components/initiative/InitiativeTrackerWindow.vue +149 -9
- package/app/components/layout/IntegrationsHub.vue +6 -19
- package/app/components/panels/inspector/InitiativeInspector.vue +23 -2
- package/app/components/settings/InfrastructureWindow.logic.spec.ts +100 -0
- package/app/components/settings/InfrastructureWindow.logic.ts +78 -0
- package/app/components/settings/InfrastructureWindow.vue +65 -46
- package/app/components/settings/PackageRegistriesPanel.vue +131 -123
- package/app/composables/useInitiativePlanning.ts +70 -0
- package/app/modular/nav-contributions.spec.ts +13 -3
- package/app/modular/nav-contributions.ts +42 -32
- package/app/pages/index.vue +0 -1
- package/app/stores/packageRegistries.spec.ts +111 -0
- package/app/stores/packageRegistries.ts +21 -8
- package/app/stores/ui/modals.ts +20 -21
- package/app/types/providerConnections.ts +12 -0
- package/app/utils/initiative.spec.ts +51 -1
- package/app/utils/initiative.ts +44 -0
- package/i18n/locales/de.json +15 -7
- package/i18n/locales/en.json +15 -7
- package/i18n/locales/es.json +15 -7
- package/i18n/locales/fr.json +15 -7
- package/i18n/locales/he.json +15 -7
- package/i18n/locales/it.json +15 -7
- package/i18n/locales/ja.json +15 -7
- package/i18n/locales/pl.json +15 -7
- package/i18n/locales/tr.json +15 -7
- package/i18n/locales/uk.json +15 -7
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// The single tabbed "Infrastructure" window — now a TOP-LEVEL navbar destination (no longer
|
|
3
|
-
// reached through the Integrations hub).
|
|
3
|
+
// reached through the Integrations hub). Its topical tabs:
|
|
4
4
|
// - "Agent containers" — where repo-operating agent containers run. Shows the execution
|
|
5
5
|
// backend selector, the runner-pool connection (ProviderConnectionTab), and — in local
|
|
6
6
|
// mode — the warm-container-pool + checkout-reuse settings (the local agent-container
|
|
@@ -10,24 +10,33 @@
|
|
|
10
10
|
// Compose setup (formerly a standalone "Environment setup" sidebar entry — it writes a
|
|
11
11
|
// service's Compose recipe plus the workspace's Compose handler, so it belongs beside the
|
|
12
12
|
// settings it edits rather than at the same level as them).
|
|
13
|
+
// - "Shared stacks" — long-lived Compose infra a Tester environment attaches to, so it rides
|
|
14
|
+
// the same probe as the environments tab (nothing to attach to without one).
|
|
15
|
+
// - "Package registries" — the private npm registries a checkout installs from (formerly an
|
|
16
|
+
// Integrations-hub row). What a container can resolve its dependencies from is part of the
|
|
17
|
+
// execution environment, not an optional external system a workspace links in.
|
|
13
18
|
// Local-specific affordances render inline, gated on `auth.localMode?.enabled`. A tab whose
|
|
14
19
|
// backend integration is disabled (503) simply doesn't render.
|
|
15
20
|
import { computed, ref, watch } from 'vue'
|
|
16
|
-
import type {
|
|
21
|
+
import type { InfrastructureTab } from '~/types/providerConnections'
|
|
22
|
+
import {
|
|
23
|
+
infrastructureTabs,
|
|
24
|
+
openInfrastructureTab,
|
|
25
|
+
repinInfrastructureTab,
|
|
26
|
+
} from '~/components/settings/InfrastructureWindow.logic'
|
|
17
27
|
import InfrastructureBackendPicker from '~/components/settings/InfrastructureBackendPicker.vue'
|
|
18
28
|
import InfraHandlersConfigurator from '~/components/settings/InfraHandlersConfigurator.vue'
|
|
19
29
|
import DefaultProvisionTypeSection from '~/components/settings/DefaultProvisionTypeSection.vue'
|
|
20
30
|
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
21
31
|
import SharedStacksPanel from '~/components/settings/SharedStacksPanel.vue'
|
|
22
32
|
import ComposeEnvironmentSetupSection from '~/components/settings/ComposeEnvironmentSetupSection.vue'
|
|
23
|
-
|
|
24
|
-
// The shared-stacks tab uses its own slot key beyond the provider-connection kinds.
|
|
25
|
-
type InfraTabValue = ProviderConnectionKind | 'shared-stacks'
|
|
33
|
+
import PackageRegistriesPanel from '~/components/settings/PackageRegistriesPanel.vue'
|
|
26
34
|
|
|
27
35
|
const { t } = useI18n()
|
|
28
36
|
const ui = useUiStore()
|
|
29
37
|
const store = useProviderConnectionsStore()
|
|
30
38
|
const auth = useAuthStore()
|
|
39
|
+
const packageRegistries = usePackageRegistriesStore()
|
|
31
40
|
|
|
32
41
|
const open = computed({
|
|
33
42
|
get: () => ui.infrastructureOpen,
|
|
@@ -45,35 +54,41 @@ const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
|
45
54
|
const agentsAvailable = computed(() => (auth.infrastructure?.execution.available.length ?? 0) > 0)
|
|
46
55
|
const envsAvailable = computed(() => (auth.infrastructure?.testEnv.available.length ?? 0) > 0)
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Shared stacks are long-lived compose infra a Tester environment attaches to, so they live
|
|
65
|
-
// alongside the test-environment config (shown wherever an environment backend is available).
|
|
66
|
-
if (envsAvailable.value)
|
|
67
|
-
out.push({
|
|
68
|
-
value: 'shared-stacks',
|
|
69
|
-
label: t('settings.sharedStacks.tab'),
|
|
70
|
-
icon: 'i-lucide-layers',
|
|
71
|
-
slot: 'shared-stacks',
|
|
72
|
-
})
|
|
73
|
-
return out
|
|
74
|
-
})
|
|
57
|
+
// Tab presentation, keyed by the closed `InfrastructureTab` union so a new tab fails to compile
|
|
58
|
+
// until it is given a label and an icon. The labels are literal `t()` calls for the same reason
|
|
59
|
+
// they are not resolved in the logic module: a key assembled at runtime is invisible to the
|
|
60
|
+
// typed-message-key check.
|
|
61
|
+
const TAB_LABELS = computed<Record<InfrastructureTab, string>>(() => ({
|
|
62
|
+
'runner-pool': t('settings.providerConnection.tabs.agentContainers'),
|
|
63
|
+
environment: t('settings.providerConnection.tabs.testEnvironments'),
|
|
64
|
+
'shared-stacks': t('settings.sharedStacks.tab'),
|
|
65
|
+
'package-registries': t('settings.packageRegistries.tab'),
|
|
66
|
+
}))
|
|
67
|
+
const TAB_ICONS: Record<InfrastructureTab, string> = {
|
|
68
|
+
'runner-pool': 'i-lucide-server-cog',
|
|
69
|
+
environment: 'i-lucide-cloud',
|
|
70
|
+
'shared-stacks': 'i-lucide-layers',
|
|
71
|
+
'package-registries': 'i-lucide-package',
|
|
72
|
+
}
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
// `slot` mirrors `value` — the template names one `<template #…>` per tab value.
|
|
75
|
+
const tabs = computed(() =>
|
|
76
|
+
infrastructureTabs({
|
|
77
|
+
agents: agentsAvailable.value,
|
|
78
|
+
environments: envsAvailable.value,
|
|
79
|
+
// The module's own probe (the backend 503s with no encryption key), same gate as the
|
|
80
|
+
// Integrations-hub row this replaced — an unconfigured backend shows no dead tab.
|
|
81
|
+
packageRegistries: packageRegistries.available === true,
|
|
82
|
+
}).map((value) => ({
|
|
83
|
+
value,
|
|
84
|
+
label: TAB_LABELS.value[value],
|
|
85
|
+
icon: TAB_ICONS[value],
|
|
86
|
+
slot: value,
|
|
87
|
+
})),
|
|
88
|
+
)
|
|
89
|
+
const tabValues = computed(() => tabs.value.map((x) => x.value))
|
|
90
|
+
|
|
91
|
+
const activeTab = ref<InfrastructureTab>(ui.infrastructureTab)
|
|
77
92
|
|
|
78
93
|
// Honour the deep-linked tab each time the window opens, falling back to the first available
|
|
79
94
|
// tab if the requested one is off.
|
|
@@ -82,24 +97,25 @@ watch(
|
|
|
82
97
|
(isOpen) => {
|
|
83
98
|
if (!isOpen) return
|
|
84
99
|
void store.ensureLoaded().catch(() => {})
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
// The registries tab gates on this probe, so it has to resolve for the tab to appear at
|
|
101
|
+
// all — the panel's own load is a no-op once this settled (`ensureLoaded` coalesces).
|
|
102
|
+
// Swallowed here on purpose: the PANEL reports a load failure, and it can only do that
|
|
103
|
+
// once the tab it lives in exists, so a probe failure has to leave the window itself alone.
|
|
104
|
+
void packageRegistries.ensureLoaded().catch(() => {})
|
|
105
|
+
activeTab.value = openInfrastructureTab(tabValues.value, ui.infrastructureTab)
|
|
88
106
|
},
|
|
89
107
|
{ immediate: true },
|
|
90
108
|
)
|
|
91
109
|
// When availability resolves after open, re-pin onto a valid tab — but keep honouring the
|
|
92
|
-
// deep-linked request. The
|
|
93
|
-
// through a transient
|
|
110
|
+
// deep-linked request. The three availability probes resolve independently, so `tabs` can pass
|
|
111
|
+
// through a transient short list; only fall back to the first tab once loading settled.
|
|
94
112
|
watch([tabs, () => store.loaded], () => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
activeTab.value = list[0]!.value
|
|
102
|
-
}
|
|
113
|
+
activeTab.value = repinInfrastructureTab(
|
|
114
|
+
tabValues.value,
|
|
115
|
+
activeTab.value,
|
|
116
|
+
ui.infrastructureTab,
|
|
117
|
+
store.loaded,
|
|
118
|
+
)
|
|
103
119
|
})
|
|
104
120
|
</script>
|
|
105
121
|
|
|
@@ -157,6 +173,9 @@ watch([tabs, () => store.loaded], () => {
|
|
|
157
173
|
<template #shared-stacks>
|
|
158
174
|
<SharedStacksPanel />
|
|
159
175
|
</template>
|
|
176
|
+
<template #package-registries>
|
|
177
|
+
<PackageRegistriesPanel />
|
|
178
|
+
</template>
|
|
160
179
|
</UTabs>
|
|
161
180
|
|
|
162
181
|
<p v-else class="px-1 py-6 text-center text-sm text-slate-500">
|
|
@@ -3,24 +3,18 @@
|
|
|
3
3
|
// orgs, GitHub Packages) that agent containers use to resolve private dependencies
|
|
4
4
|
// on checkout. Tokens are write-only: the list renders from the redacted summary
|
|
5
5
|
// (vendor + scopes + token tail) and an entry is edited by deleting + re-adding.
|
|
6
|
-
//
|
|
7
|
-
|
|
6
|
+
// Renders inline inside the Infrastructure window's "Package registries" tab: what a
|
|
7
|
+
// checkout installs from is part of where agent containers RUN, not an optional
|
|
8
|
+
// external system the workspace links in.
|
|
9
|
+
import { computed, onMounted, reactive, ref } from 'vue'
|
|
8
10
|
import type { PackageRegistryVendor } from '~/types/packageRegistries'
|
|
9
|
-
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
10
11
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
11
12
|
|
|
12
13
|
const { t } = useI18n()
|
|
13
|
-
const ui = useUiStore()
|
|
14
14
|
const store = usePackageRegistriesStore()
|
|
15
15
|
const toast = useToast()
|
|
16
16
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
17
17
|
|
|
18
|
-
const open = computed({
|
|
19
|
-
get: () => ui.packageRegistriesOpen,
|
|
20
|
-
set: (v: boolean) => (v ? ui.openPackageRegistries() : ui.closePackageRegistries()),
|
|
21
|
-
})
|
|
22
|
-
const back = useIntegrationBack(open)
|
|
23
|
-
|
|
24
18
|
// The registry vendors a workspace can connect. Fixed set — the host derives from the
|
|
25
19
|
// vendor server-side, so it renders read-only here. Vendor names stay verbatim.
|
|
26
20
|
const VENDORS: { value: PackageRegistryVendor; label: string; host: string }[] = [
|
|
@@ -59,18 +53,19 @@ function notifyError(title: string, e: unknown) {
|
|
|
59
53
|
})
|
|
60
54
|
}
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
// The tab this renders in only exists once the window's probe resolved `available === true`, so
|
|
57
|
+
// `ensureLoaded()` here would early-return every time and this error branch would be dead code.
|
|
58
|
+
// Read the list outright instead: the window owns the PROBE (a failure there means no tab), the
|
|
59
|
+
// panel owns the DATA (a failure here means the reader is looking at a list we could not fetch,
|
|
60
|
+
// and must be told). It also drops the staleness `ensureLoaded` carried — reopening the tab
|
|
61
|
+
// after an entry was added elsewhere used to re-render the first load's snapshot.
|
|
62
|
+
onMounted(async () => {
|
|
63
|
+
try {
|
|
64
|
+
await store.load()
|
|
65
|
+
} catch (e) {
|
|
66
|
+
notifyError(t('settings.packageRegistries.toast.loadFailed'), e)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
74
69
|
|
|
75
70
|
async function addEntry() {
|
|
76
71
|
busy.value = true
|
|
@@ -111,108 +106,121 @@ async function removeEntry(entryId: string) {
|
|
|
111
106
|
</script>
|
|
112
107
|
|
|
113
108
|
<template>
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<div
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</
|
|
135
|
-
<div
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<div class="flex items-center gap-2">
|
|
142
|
-
<span class="text-sm font-medium">{{ vendorLabel(entry.vendor) }}</span>
|
|
143
|
-
<span class="text-[11px] text-slate-500">
|
|
144
|
-
{{ t('settings.packageRegistries.list.tokenTail', { tail: entry.tokenTail }) }}
|
|
145
|
-
</span>
|
|
146
|
-
</div>
|
|
147
|
-
<div class="flex flex-wrap gap-1">
|
|
148
|
-
<UBadge
|
|
149
|
-
v-for="scope in entry.scopes"
|
|
150
|
-
:key="scope"
|
|
151
|
-
color="neutral"
|
|
152
|
-
variant="soft"
|
|
153
|
-
size="sm"
|
|
154
|
-
>
|
|
155
|
-
{{ scope }}
|
|
156
|
-
</UBadge>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
<UButton
|
|
160
|
-
color="error"
|
|
161
|
-
variant="ghost"
|
|
162
|
-
icon="i-lucide-trash-2"
|
|
109
|
+
<div class="space-y-4" data-testid="package-registries-panel">
|
|
110
|
+
<p class="text-sm text-slate-400">
|
|
111
|
+
{{ t('settings.packageRegistries.intro') }}
|
|
112
|
+
</p>
|
|
113
|
+
|
|
114
|
+
<section v-if="store.entries.length" class="space-y-2 rounded-lg border border-slate-700 p-3">
|
|
115
|
+
<h3 class="text-sm font-semibold">
|
|
116
|
+
{{ t('settings.packageRegistries.list.heading') }}
|
|
117
|
+
</h3>
|
|
118
|
+
<div
|
|
119
|
+
v-for="entry in store.entries"
|
|
120
|
+
:key="entry.id"
|
|
121
|
+
class="flex items-center justify-between gap-2 rounded-md border border-slate-800 px-3 py-2"
|
|
122
|
+
>
|
|
123
|
+
<div class="min-w-0 space-y-1">
|
|
124
|
+
<div class="flex items-center gap-2">
|
|
125
|
+
<span class="text-sm font-medium">{{ vendorLabel(entry.vendor) }}</span>
|
|
126
|
+
<span class="text-[11px] text-slate-500">
|
|
127
|
+
{{ t('settings.packageRegistries.list.tokenTail', { tail: entry.tokenTail }) }}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="flex flex-wrap gap-1">
|
|
131
|
+
<UBadge
|
|
132
|
+
v-for="scope in entry.scopes"
|
|
133
|
+
:key="scope"
|
|
134
|
+
color="neutral"
|
|
135
|
+
variant="soft"
|
|
163
136
|
size="sm"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
137
|
+
>
|
|
138
|
+
{{ scope }}
|
|
139
|
+
</UBadge>
|
|
140
|
+
<!-- An entry with no scopes authenticates its host without routing any scope to
|
|
141
|
+
it (the deliberate mixed public/private setup) — say so, or the row reads as
|
|
142
|
+
half-configured. -->
|
|
143
|
+
<span v-if="!entry.scopes.length" class="text-[11px] text-slate-500">
|
|
144
|
+
{{ t('settings.packageRegistries.list.noScopes') }}
|
|
145
|
+
</span>
|
|
169
146
|
</div>
|
|
170
|
-
</
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
147
|
+
</div>
|
|
148
|
+
<UButton
|
|
149
|
+
color="error"
|
|
150
|
+
variant="ghost"
|
|
151
|
+
icon="i-lucide-trash-2"
|
|
152
|
+
size="sm"
|
|
153
|
+
:loading="busy"
|
|
154
|
+
:data-testid="`package-registry-delete-${entry.id}`"
|
|
155
|
+
:aria-label="t('settings.packageRegistries.list.remove')"
|
|
156
|
+
@click="removeEntry(entry.id)"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
|
|
161
|
+
<section class="space-y-3 rounded-lg border border-slate-700 p-3">
|
|
162
|
+
<h3 class="text-sm font-semibold">
|
|
163
|
+
{{ t('settings.packageRegistries.add.heading') }}
|
|
164
|
+
</h3>
|
|
165
|
+
|
|
166
|
+
<UFormField :label="t('settings.packageRegistries.add.vendor')">
|
|
167
|
+
<USelect
|
|
168
|
+
v-model="form.vendor"
|
|
169
|
+
:items="VENDORS"
|
|
170
|
+
value-key="value"
|
|
171
|
+
class="w-full"
|
|
172
|
+
data-testid="package-registry-vendor"
|
|
173
|
+
/>
|
|
174
|
+
</UFormField>
|
|
175
|
+
<p class="text-[11px] text-slate-500">
|
|
176
|
+
{{ t('settings.packageRegistries.add.host', { host: vendorHost }) }}
|
|
177
|
+
</p>
|
|
178
|
+
|
|
179
|
+
<UFormField
|
|
180
|
+
:label="t('settings.packageRegistries.add.scopes')"
|
|
181
|
+
:help="t('settings.packageRegistries.add.scopesHelp')"
|
|
182
|
+
>
|
|
183
|
+
<UInput
|
|
184
|
+
v-model="form.scopes"
|
|
185
|
+
placeholder="@my-org, @my-other-org"
|
|
186
|
+
class="w-full"
|
|
187
|
+
data-testid="package-registry-scopes"
|
|
188
|
+
/>
|
|
189
|
+
</UFormField>
|
|
190
|
+
<!-- What will actually be SAVED. The parse splits on commas/whitespace and prefixes a
|
|
191
|
+
missing `@`, so the field's text and the stored scopes routinely differ — and now
|
|
192
|
+
that an empty list is a legitimate save, "I typed something that parsed to nothing"
|
|
193
|
+
and "I meant to leave this empty" would otherwise look identical at the button. -->
|
|
194
|
+
<div v-if="parsedScopes.length" class="flex flex-wrap gap-1">
|
|
195
|
+
<UBadge
|
|
196
|
+
v-for="scope in parsedScopes"
|
|
197
|
+
:key="scope"
|
|
198
|
+
color="neutral"
|
|
199
|
+
variant="soft"
|
|
200
|
+
size="sm"
|
|
201
|
+
data-testid="package-registry-parsed-scope"
|
|
202
|
+
>
|
|
203
|
+
{{ scope }}
|
|
204
|
+
</UBadge>
|
|
215
205
|
</div>
|
|
216
|
-
|
|
217
|
-
|
|
206
|
+
<!-- Why leaving this empty is often the RIGHT answer — a scope mapping is all-or-nothing,
|
|
207
|
+
so an org publishing some of its `@org` packages publicly breaks under one. -->
|
|
208
|
+
<p class="text-[11px] text-slate-500">
|
|
209
|
+
{{ t('settings.packageRegistries.add.scopesNote') }}
|
|
210
|
+
</p>
|
|
211
|
+
|
|
212
|
+
<UFormField :label="t('settings.packageRegistries.add.token')">
|
|
213
|
+
<SecretInput v-model="form.token" class="w-full" data-testid="package-registry-token" />
|
|
214
|
+
</UFormField>
|
|
215
|
+
|
|
216
|
+
<UButton
|
|
217
|
+
:loading="busy"
|
|
218
|
+
:disabled="!form.token.trim()"
|
|
219
|
+
data-testid="package-registry-save"
|
|
220
|
+
@click="addEntry"
|
|
221
|
+
>
|
|
222
|
+
{{ t('settings.packageRegistries.add.save') }}
|
|
223
|
+
</UButton>
|
|
224
|
+
</section>
|
|
225
|
+
</div>
|
|
218
226
|
</template>
|
|
@@ -4,12 +4,25 @@ import { useExecutionStore } from '~/stores/execution'
|
|
|
4
4
|
import { useInitiativesStore } from '~/stores/initiative'
|
|
5
5
|
import { usePipelinesStore } from '~/stores/pipelines'
|
|
6
6
|
import { useUiStore } from '~/stores/ui'
|
|
7
|
+
import { agentKindMeta } from '~/utils/catalog'
|
|
8
|
+
import { selectPlanApproval, type InitiativeAttentionKind } from '~/utils/initiative'
|
|
7
9
|
import {
|
|
8
10
|
INITIATIVE_INTERVIEWER_KIND,
|
|
9
11
|
interviewGatePhase,
|
|
10
12
|
interviewStepReached,
|
|
11
13
|
} from '~/utils/interviewGate'
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* What an initiative's planning run needs from a human right now: which kind of park it is (the
|
|
17
|
+
* card + inspector resolve its icon/label from the shared `INITIATIVE_ATTENTION_*` maps, so the
|
|
18
|
+
* two surfaces word one park identically), and the action that opens the surface which can
|
|
19
|
+
* RESOLVE it — the step's own dedicated window, via `dispatchStepView`.
|
|
20
|
+
*/
|
|
21
|
+
export interface InitiativeAttention {
|
|
22
|
+
kind: InitiativeAttentionKind
|
|
23
|
+
open: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
/**
|
|
14
27
|
* Shared planning affordances for an `initiative`-level block, used by BOTH the board card
|
|
15
28
|
* (`InitiativeCard`) and the inspector (`InitiativeInspector`) so the two surfaces can never drift
|
|
@@ -83,6 +96,60 @@ export function useInitiativePlanning(blockId: MaybeRefOrGetter<string>) {
|
|
|
83
96
|
() => interviewPhase.value === 'working' || interviewPhase.value === 'preparing',
|
|
84
97
|
)
|
|
85
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The planner's parked plan-approval gate, or undefined. `pl_initiative` gates the planner step
|
|
101
|
+
* (`{ kind: 'initiative-planner', gate: true }`), so a finished planning pass PARKS the run on a
|
|
102
|
+
* pending `step.approval` until a human accepts the plan — the state this composable's other
|
|
103
|
+
* flags deliberately do NOT cover (the interview has converged, and the run is `blocked`, so
|
|
104
|
+
* `awaitingAnswers` and `interviewing` are both false and the card would otherwise sit on a
|
|
105
|
+
* disabled, spinning "Run planning").
|
|
106
|
+
*
|
|
107
|
+
* The interviewer's own park is excluded by the window its step routes to (see
|
|
108
|
+
* {@link selectPlanApproval}), not by the interview phase, so the two affordances can never both
|
|
109
|
+
* claim one park.
|
|
110
|
+
*/
|
|
111
|
+
const planApproval = computed(() =>
|
|
112
|
+
selectPlanApproval(
|
|
113
|
+
execution.approvalsByBlock.get(toValue(blockId)) ?? [],
|
|
114
|
+
(kind) => agentKindMeta(kind).resultView,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/** An agent-raised decision on the planning run (the analyst is an ordinary agent step). */
|
|
119
|
+
const pendingDecision = computed(() => execution.decisionsByBlock.get(toValue(blockId))?.[0])
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The single thing a human has to act on, or null — the initiative dual of a task card's
|
|
123
|
+
* `attention`. A decision outranks an approval (a step never holds both; this is just a stable
|
|
124
|
+
* order). Opening always goes through the step-view dispatch, so the park lands in the window
|
|
125
|
+
* that can RESOLVE it (the plan gate → the tracker window's plan-review rail) rather than a
|
|
126
|
+
* generic panel that would refuse it.
|
|
127
|
+
*/
|
|
128
|
+
const attention = computed<InitiativeAttention | null>(() => {
|
|
129
|
+
const id = toValue(blockId)
|
|
130
|
+
const decision = pendingDecision.value
|
|
131
|
+
if (decision) {
|
|
132
|
+
return {
|
|
133
|
+
kind: 'decision',
|
|
134
|
+
open: () => {
|
|
135
|
+
ui.select(id)
|
|
136
|
+
ui.openDecision(decision.instanceId, decision.decision.id)
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const approval = planApproval.value
|
|
141
|
+
if (approval) {
|
|
142
|
+
return {
|
|
143
|
+
kind: 'approval',
|
|
144
|
+
open: () => {
|
|
145
|
+
ui.select(id)
|
|
146
|
+
ui.openApprovalDetail(approval.instanceId, approval.approval.id)
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
})
|
|
152
|
+
|
|
86
153
|
/**
|
|
87
154
|
* Optimistic start flag: flip true the instant "Run planning" is clicked, before the stream
|
|
88
155
|
* pushes the block's `executionId` back. Cleared the moment `running` takes over (success) or the
|
|
@@ -123,6 +190,9 @@ export function useInitiativePlanning(blockId: MaybeRefOrGetter<string>) {
|
|
|
123
190
|
interviewPhase,
|
|
124
191
|
awaitingAnswers,
|
|
125
192
|
interviewing,
|
|
193
|
+
planApproval,
|
|
194
|
+
pendingDecision,
|
|
195
|
+
attention,
|
|
126
196
|
starting,
|
|
127
197
|
runPlanning,
|
|
128
198
|
openPlanning,
|
|
@@ -285,10 +285,20 @@ describe('nav grouping helpers', () => {
|
|
|
285
285
|
'workspaceContext',
|
|
286
286
|
'configuration',
|
|
287
287
|
])
|
|
288
|
-
// The
|
|
289
|
-
//
|
|
288
|
+
// The model layer is its own section, ahead of the optional integrations: the engines,
|
|
289
|
+
// the per-agent model choice (beside the providers it picks from, rather than under
|
|
290
|
+
// `configuration`), and the two surfaces that evaluate a prompt+agent+model. Sandbox and
|
|
291
|
+
// Kaizen used to sit under `integrations`, which read as a claim they connect to an
|
|
292
|
+
// external system; they don't — they exercise and grade what this section configures.
|
|
290
293
|
const models = groups.find((g) => g.group === 'models')
|
|
291
|
-
expect(models?.items.map((i) => i.id)).toEqual([
|
|
294
|
+
expect(models?.items.map((i) => i.id)).toEqual([
|
|
295
|
+
'model-providers',
|
|
296
|
+
'model-config',
|
|
297
|
+
'sandbox',
|
|
298
|
+
'kaizen',
|
|
299
|
+
])
|
|
300
|
+
const integrations = groups.find((g) => g.group === 'integrations')
|
|
301
|
+
expect(integrations?.items.map((i) => i.id)).toEqual(['integrations-hub'])
|
|
292
302
|
const configuration = groups.find((g) => g.group === 'configuration')
|
|
293
303
|
expect(configuration?.items.map((i) => i.id)).toEqual([
|
|
294
304
|
'workspace-settings',
|