@cat-factory/app 0.32.2 → 0.34.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/fragments/FragmentLibraryManager.vue +511 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +12 -458
- package/app/components/layout/AccountFragmentSettings.vue +37 -0
- package/app/components/layout/AccountTeamSettings.vue +58 -9
- package/app/components/layout/BoardSwitcher.vue +9 -20
- package/app/components/layout/CommandBar.vue +9 -0
- package/app/components/layout/SideBar.vue +15 -0
- package/app/components/settings/AccountSettingsPanel.vue +57 -0
- package/app/components/settings/ProviderConnectionPanel.vue +117 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +21 -0
- package/app/composables/api/fragments.ts +12 -3
- package/app/pages/index.vue +2 -0
- package/app/stores/fragmentLibrary.ts +80 -31
- package/app/stores/ui.ts +23 -0
- package/app/stores/workspaceSettings.ts +2 -0
- package/app/types/domain.ts +6 -0
- package/package.json +1 -1
|
@@ -173,6 +173,15 @@ const commands = computed<Command[]>(() => {
|
|
|
173
173
|
keywords: 'fragment best practice guideline service default code-aware',
|
|
174
174
|
run: () => ui.openWorkspaceSettings('fragments'),
|
|
175
175
|
})
|
|
176
|
+
list.push({
|
|
177
|
+
id: 'account-settings',
|
|
178
|
+
label: 'Account settings',
|
|
179
|
+
group: 'Account',
|
|
180
|
+
icon: 'i-lucide-settings',
|
|
181
|
+
keywords:
|
|
182
|
+
'account team members roles invitations email api keys fragment best practice library context organization personal',
|
|
183
|
+
run: () => ui.openAccountSettings(),
|
|
184
|
+
})
|
|
176
185
|
list.push({
|
|
177
186
|
id: 'local-models',
|
|
178
187
|
label: 'My local runners',
|
|
@@ -14,6 +14,7 @@ const github = useGitHubStore()
|
|
|
14
14
|
const slack = useSlackStore()
|
|
15
15
|
const library = useFragmentLibraryStore()
|
|
16
16
|
const workspace = useWorkspaceStore()
|
|
17
|
+
const accounts = useAccountsStore()
|
|
17
18
|
const ui = useUiStore()
|
|
18
19
|
|
|
19
20
|
// Resolve whether the document-source / task-source / GitHub integrations are
|
|
@@ -203,6 +204,20 @@ watch(
|
|
|
203
204
|
>
|
|
204
205
|
Model Configuration
|
|
205
206
|
</UButton>
|
|
207
|
+
<!-- Account & team: members + roles, invitations, email sender, account API keys.
|
|
208
|
+
Shown once accounts (auth) are enabled. -->
|
|
209
|
+
<UButton
|
|
210
|
+
v-if="accounts.enabled"
|
|
211
|
+
block
|
|
212
|
+
color="primary"
|
|
213
|
+
variant="soft"
|
|
214
|
+
size="sm"
|
|
215
|
+
icon="i-lucide-users"
|
|
216
|
+
class="justify-start"
|
|
217
|
+
@click="ui.openAccountSettings()"
|
|
218
|
+
>
|
|
219
|
+
Account settings
|
|
220
|
+
</UButton>
|
|
206
221
|
</div>
|
|
207
222
|
</section>
|
|
208
223
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Account settings — a single tabbed modal for the per-account configuration, distinct
|
|
3
|
+
// from Workspace settings. Hosts the team panel (members + roles, invitations, email
|
|
4
|
+
// sender, account-wide API keys; org-scoped, with a create-org CTA on a personal account)
|
|
5
|
+
// and the account-tier prompt-fragment library (available for every account type).
|
|
6
|
+
// Opened from the SideBar Configuration section, the account switcher and the command
|
|
7
|
+
// bar; bound to the `ui` store so any surface can open it and deep-link to a tab.
|
|
8
|
+
import AccountTeamSettings from '~/components/layout/AccountTeamSettings.vue'
|
|
9
|
+
import AccountFragmentSettings from '~/components/layout/AccountFragmentSettings.vue'
|
|
10
|
+
|
|
11
|
+
const ui = useUiStore()
|
|
12
|
+
const accounts = useAccountsStore()
|
|
13
|
+
|
|
14
|
+
const open = computed({
|
|
15
|
+
get: () => ui.accountSettingsOpen,
|
|
16
|
+
set: (v: boolean) => (v ? ui.openAccountSettings() : ui.closeAccountSettings()),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Driven by the ui store so other surfaces (command bar, the workspace-settings
|
|
20
|
+
// cross-link) can deep-link straight to a tab.
|
|
21
|
+
const activeTab = computed({
|
|
22
|
+
get: () => ui.accountSettingsTab,
|
|
23
|
+
set: (v: string) => ui.setAccountSettingsTab(v),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const tabs = [
|
|
27
|
+
{ value: 'team', label: 'Team & access', icon: 'i-lucide-users', slot: 'team' },
|
|
28
|
+
{
|
|
29
|
+
value: 'fragments',
|
|
30
|
+
label: 'Context fragments',
|
|
31
|
+
icon: 'i-lucide-book-marked',
|
|
32
|
+
slot: 'fragments',
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<UModal v-model:open="open" title="Account settings" :ui="{ content: 'max-w-3xl' }">
|
|
39
|
+
<template #body>
|
|
40
|
+
<p v-if="!accounts.activeAccountId" class="text-sm text-slate-400">No account selected.</p>
|
|
41
|
+
<UTabs
|
|
42
|
+
v-else
|
|
43
|
+
v-model="activeTab"
|
|
44
|
+
:items="tabs"
|
|
45
|
+
variant="link"
|
|
46
|
+
:ui="{ root: 'gap-4', list: 'overflow-x-auto' }"
|
|
47
|
+
>
|
|
48
|
+
<template #team>
|
|
49
|
+
<AccountTeamSettings :account-id="accounts.activeAccountId" />
|
|
50
|
+
</template>
|
|
51
|
+
<template #fragments>
|
|
52
|
+
<AccountFragmentSettings :account-id="accounts.activeAccountId" />
|
|
53
|
+
</template>
|
|
54
|
+
</UTabs>
|
|
55
|
+
</template>
|
|
56
|
+
</UModal>
|
|
57
|
+
</template>
|
|
@@ -44,6 +44,41 @@ const meta = computed(() => (kind.value ? META[kind.value] : null))
|
|
|
44
44
|
const descriptor = computed(() => (kind.value ? store.descriptorFor(kind.value) : null))
|
|
45
45
|
const connection = computed(() => (kind.value ? store.connectionFor(kind.value) : null))
|
|
46
46
|
|
|
47
|
+
// --- Local-mode infrastructure delegation -------------------------------------------
|
|
48
|
+
// In local mode this same screen is where a developer chooses, per workspace, whether to
|
|
49
|
+
// run on this machine (host Docker for agents, in-container docker-compose for the Tester)
|
|
50
|
+
// or delegate to an external service. The two opt-ins live here together to make the
|
|
51
|
+
// cross-cutting nature explicit: the environment provider you configure on this screen is
|
|
52
|
+
// one half; the runner pool (its own screen) is the other. Each toggle is enabled only
|
|
53
|
+
// once its provider is registered. Shown only in local mode and only on the environment
|
|
54
|
+
// kind (so it appears once, alongside the env provider it relates to).
|
|
55
|
+
const auth = useAuthStore()
|
|
56
|
+
const settings = useWorkspaceSettingsStore()
|
|
57
|
+
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
58
|
+
const showLocalDelegation = computed(() => isLocal.value && kind.value === 'environment')
|
|
59
|
+
// Gating: a toggle's external option is selectable only when its provider is registered.
|
|
60
|
+
const runnerPoolRegistered = computed(() => !!store.connectionFor('runner-pool'))
|
|
61
|
+
const envRegistered = computed(() => !!store.connectionFor('environment'))
|
|
62
|
+
const savingDelegation = ref(false)
|
|
63
|
+
|
|
64
|
+
async function setDelegation(patch: {
|
|
65
|
+
delegateAgentsToRunnerPool?: boolean
|
|
66
|
+
delegateTestEnvToProvider?: boolean
|
|
67
|
+
}) {
|
|
68
|
+
savingDelegation.value = true
|
|
69
|
+
try {
|
|
70
|
+
await settings.update(patch)
|
|
71
|
+
} catch (e) {
|
|
72
|
+
notifyError('Could not update delegation', e)
|
|
73
|
+
} finally {
|
|
74
|
+
savingDelegation.value = false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function openRunnerPoolPanel() {
|
|
79
|
+
ui.openProviderConnection('runner-pool')
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
// "View logs": the provisioning event history for this provider's subsystem — every
|
|
48
83
|
// spin-up / tear-down attempt with its outcome and the exact error. The panel kind
|
|
49
84
|
// maps 1:1 to the log subsystem ('environment' / 'runner-pool').
|
|
@@ -82,6 +117,9 @@ watch(
|
|
|
82
117
|
kind,
|
|
83
118
|
(k) => {
|
|
84
119
|
if (k) void store.loadKind(k).then(resetDraft)
|
|
120
|
+
// In local mode the env panel also gates the agents toggle on a registered runner
|
|
121
|
+
// pool, so load that provider's connection state too (the env kind already loads above).
|
|
122
|
+
if (k === 'environment' && isLocal.value) void store.loadKind('runner-pool')
|
|
85
123
|
},
|
|
86
124
|
{ immediate: true },
|
|
87
125
|
)
|
|
@@ -228,6 +266,85 @@ function fieldHelp(key: string): string | undefined {
|
|
|
228
266
|
<IntegrationBackTitle :title="meta?.title ?? 'Provider'" @back="back" />
|
|
229
267
|
</template>
|
|
230
268
|
<template #body>
|
|
269
|
+
<!-- Local-mode infrastructure delegation: the local-vs-external choice for BOTH
|
|
270
|
+
container agents AND the Tester's ephemeral environments, made once here. -->
|
|
271
|
+
<section
|
|
272
|
+
v-if="showLocalDelegation"
|
|
273
|
+
class="mb-4 space-y-3 rounded-lg border border-slate-700 bg-slate-900/40 p-3"
|
|
274
|
+
>
|
|
275
|
+
<div>
|
|
276
|
+
<h3 class="text-sm font-semibold text-slate-200">Local delegation</h3>
|
|
277
|
+
<p class="mt-1 text-[11px] text-slate-400">
|
|
278
|
+
By default this machine runs everything locally — container agents on host Docker, the
|
|
279
|
+
Tester's infrastructure via in-container docker-compose. Opt in below to delegate either
|
|
280
|
+
concern to an external service instead. Applies only in local mode.
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<!-- Container agents → self-hosted runner pool -->
|
|
285
|
+
<div class="space-y-1">
|
|
286
|
+
<label class="flex items-center gap-2">
|
|
287
|
+
<USwitch
|
|
288
|
+
size="sm"
|
|
289
|
+
:model-value="settings.settings.delegateAgentsToRunnerPool"
|
|
290
|
+
:disabled="savingDelegation || !runnerPoolRegistered"
|
|
291
|
+
@update:model-value="(v) => setDelegation({ delegateAgentsToRunnerPool: v })"
|
|
292
|
+
/>
|
|
293
|
+
<span class="text-sm text-slate-200">Run container agents on the runner pool</span>
|
|
294
|
+
</label>
|
|
295
|
+
<p class="pl-9 text-[11px] text-slate-400">
|
|
296
|
+
Dispatch every container agent (coder, tester, merger, bootstrap, …) to this workspace's
|
|
297
|
+
self-hosted runner pool instead of host Docker.
|
|
298
|
+
<template v-if="!runnerPoolRegistered">
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
|
|
302
|
+
@click="openRunnerPoolPanel"
|
|
303
|
+
>
|
|
304
|
+
Register a runner pool
|
|
305
|
+
</button>
|
|
306
|
+
first to enable this.
|
|
307
|
+
</template>
|
|
308
|
+
</p>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<!-- Tester environments → environment provider -->
|
|
312
|
+
<div class="space-y-1">
|
|
313
|
+
<label class="flex items-center gap-2">
|
|
314
|
+
<USwitch
|
|
315
|
+
size="sm"
|
|
316
|
+
:model-value="settings.settings.delegateTestEnvToProvider"
|
|
317
|
+
:disabled="savingDelegation || !envRegistered"
|
|
318
|
+
@update:model-value="(v) => setDelegation({ delegateTestEnvToProvider: v })"
|
|
319
|
+
/>
|
|
320
|
+
<span class="text-sm text-slate-200">
|
|
321
|
+
Provision Tester environments via the provider
|
|
322
|
+
</span>
|
|
323
|
+
</label>
|
|
324
|
+
<p class="pl-9 text-[11px] text-slate-400">
|
|
325
|
+
Stand the Tester's preview environment up through the environment provider configured
|
|
326
|
+
below instead of in-container docker-compose. Connect a provider first to enable this.
|
|
327
|
+
</p>
|
|
328
|
+
</div>
|
|
329
|
+
</section>
|
|
330
|
+
|
|
331
|
+
<!-- In local mode the local-vs-external toggle for agents lives on the Ephemeral
|
|
332
|
+
environments screen (alongside the env toggle), so they're configured together. -->
|
|
333
|
+
<p
|
|
334
|
+
v-if="isLocal && kind === 'runner-pool'"
|
|
335
|
+
class="mb-4 rounded-md border border-slate-700 bg-slate-900/40 px-3 py-2 text-[11px] text-slate-400"
|
|
336
|
+
>
|
|
337
|
+
Register your pool here, then enable "Run container agents on the runner pool" on the
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
|
|
341
|
+
@click="ui.openProviderConnection('environment')"
|
|
342
|
+
>
|
|
343
|
+
Ephemeral environments
|
|
344
|
+
</button>
|
|
345
|
+
screen to route this workspace's agents to it.
|
|
346
|
+
</p>
|
|
347
|
+
|
|
231
348
|
<div v-if="descriptor" class="space-y-4">
|
|
232
349
|
<div class="flex items-start justify-between gap-3">
|
|
233
350
|
<p class="text-xs text-slate-400">{{ meta?.blurb }}</p>
|
|
@@ -8,6 +8,7 @@ import { onMounted, ref } from 'vue'
|
|
|
8
8
|
|
|
9
9
|
const fragments = useFragmentsStore()
|
|
10
10
|
const defaults = useServiceFragmentDefaultsStore()
|
|
11
|
+
const ui = useUiStore()
|
|
11
12
|
const toast = useToast()
|
|
12
13
|
|
|
13
14
|
const busy = ref(false)
|
|
@@ -105,5 +106,25 @@ function remove(id: string) {
|
|
|
105
106
|
<p v-else class="text-[11px] text-slate-500">
|
|
106
107
|
None — new services start with no service-level fragments.
|
|
107
108
|
</p>
|
|
109
|
+
|
|
110
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1 border-t border-slate-800 pt-3 text-[11px]">
|
|
111
|
+
<span class="text-slate-500">
|
|
112
|
+
Need to add or edit the fragments themselves (hand-authored, linked docs, repos)?
|
|
113
|
+
</span>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
class="font-medium text-primary-400 hover:underline"
|
|
117
|
+
@click="ui.openFragmentLibrary()"
|
|
118
|
+
>
|
|
119
|
+
Manage this board's fragment library →
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
class="font-medium text-primary-400 hover:underline"
|
|
124
|
+
@click="ui.openAccountSettings('fragments')"
|
|
125
|
+
>
|
|
126
|
+
Manage account fragments →
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
108
129
|
</div>
|
|
109
130
|
</template>
|
|
@@ -53,10 +53,19 @@ export function fragmentsApi({ http, ws, scope }: ApiContext) {
|
|
|
53
53
|
body: CreateDocumentFragmentInput,
|
|
54
54
|
) => http<PromptFragment>(`${scope(kind, id)}/document-fragments`, { method: 'POST', body }),
|
|
55
55
|
|
|
56
|
-
// Force an immediate live re-resolve of a document-backed fragment.
|
|
57
|
-
|
|
56
|
+
// Force an immediate live re-resolve of a document-backed fragment. At the
|
|
57
|
+
// account scope the backend needs a `viaWorkspaceId` (the workspace whose
|
|
58
|
+
// document-source connection to fetch through); it is ignored at workspace scope.
|
|
59
|
+
refreshFragment: (
|
|
60
|
+
kind: FragmentOwnerKind,
|
|
61
|
+
id: string,
|
|
62
|
+
fragmentId: string,
|
|
63
|
+
viaWorkspaceId?: string,
|
|
64
|
+
) =>
|
|
58
65
|
http<PromptFragment>(
|
|
59
|
-
`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}/refresh
|
|
66
|
+
`${scope(kind, id)}/prompt-fragments/${encodeURIComponent(fragmentId)}/refresh${
|
|
67
|
+
viaWorkspaceId ? `?viaWorkspaceId=${encodeURIComponent(viaWorkspaceId)}` : ''
|
|
68
|
+
}`,
|
|
60
69
|
{ method: 'POST' },
|
|
61
70
|
),
|
|
62
71
|
|
package/app/pages/index.vue
CHANGED
|
@@ -29,6 +29,7 @@ import FragmentLibraryPanel from '~/components/fragments/FragmentLibraryPanel.vu
|
|
|
29
29
|
import CommandBar from '~/components/layout/CommandBar.vue'
|
|
30
30
|
import IntegrationsHub from '~/components/layout/IntegrationsHub.vue'
|
|
31
31
|
import WorkspaceSettingsPanel from '~/components/settings/WorkspaceSettingsPanel.vue'
|
|
32
|
+
import AccountSettingsPanel from '~/components/settings/AccountSettingsPanel.vue'
|
|
32
33
|
import ObservabilityConnectionPanel from '~/components/settings/ObservabilityConnectionPanel.vue'
|
|
33
34
|
import ProviderConnectionPanel from '~/components/settings/ProviderConnectionPanel.vue'
|
|
34
35
|
import ProviderConfigBanner from '~/components/layout/ProviderConfigBanner.vue'
|
|
@@ -188,6 +189,7 @@ watch(
|
|
|
188
189
|
<CommandBar />
|
|
189
190
|
<IntegrationsHub />
|
|
190
191
|
<WorkspaceSettingsPanel />
|
|
192
|
+
<AccountSettingsPanel />
|
|
191
193
|
<ObservabilityConnectionPanel />
|
|
192
194
|
<ProviderConnectionPanel />
|
|
193
195
|
<ModelConfigurationPanel />
|
|
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
|
|
|
3
3
|
import type {
|
|
4
4
|
CreateDocumentFragmentInput,
|
|
5
5
|
CreatePromptFragmentInput,
|
|
6
|
+
FragmentOwnerKind,
|
|
6
7
|
FragmentSource,
|
|
7
8
|
LinkFragmentSourceInput,
|
|
8
9
|
PromptFragment,
|
|
@@ -12,40 +13,62 @@ import type {
|
|
|
12
13
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* Prompt-fragment library state (ADR 0006), scoped to
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* Prompt-fragment library state (ADR 0006), scoped to a single owner — a board
|
|
17
|
+
* (`workspace`) or an account. Holds that owner's own (raw) tier fragments, its
|
|
18
|
+
* linked guideline repos, and — for the **workspace** tier only — the merged
|
|
19
|
+
* catalog an agent actually sees (built-in ∪ account ∪ workspace). `available`
|
|
20
|
+
* mirrors the backend's opt-in gate: a 503 from the probe means the feature is off
|
|
21
|
+
* and the UI hides its entry.
|
|
22
|
+
*
|
|
23
|
+
* Two entry points share this setup: `useFragmentLibraryStore` (the workspace
|
|
24
|
+
* singleton that follows the active board, used by the navbar + the board modal)
|
|
25
|
+
* and `useFragmentLibrary(kind, ownerId)` (an owner-keyed store, used for the
|
|
26
|
+
* account tier). The account tier has no resolved/merged catalog and, for
|
|
27
|
+
* document-backed fragments, needs a `viaWorkspaceId` (document-source credentials
|
|
28
|
+
* are per-workspace).
|
|
21
29
|
*/
|
|
22
|
-
|
|
30
|
+
function fragmentLibrarySetup(kind: FragmentOwnerKind, resolveOwnerId: () => string | null) {
|
|
23
31
|
const api = useApi()
|
|
24
|
-
|
|
32
|
+
|
|
33
|
+
/** The merged/resolved catalog only exists at the workspace tier. */
|
|
34
|
+
const hasResolved = kind === 'workspace'
|
|
25
35
|
|
|
26
36
|
/** null = not probed yet; true/false = library on/off. */
|
|
27
37
|
const available = ref<boolean | null>(null)
|
|
28
|
-
/** This
|
|
38
|
+
/** This owner's hand-authored + sourced fragments (its own tier, raw). */
|
|
29
39
|
const fragments = ref<PromptFragment[]>([])
|
|
30
|
-
/** The merged catalog an agent sees (
|
|
40
|
+
/** The merged catalog an agent sees (workspace tier only; empty otherwise). */
|
|
31
41
|
const resolved = ref<ResolvedFragment[]>([])
|
|
32
|
-
/** Linked guideline repos for this
|
|
42
|
+
/** Linked guideline repos for this owner. */
|
|
33
43
|
const sources = ref<FragmentSource[]>([])
|
|
34
44
|
/** Per-source "changes available" counts from the last status check. */
|
|
35
45
|
const sourceChanges = ref<Record<string, number>>({})
|
|
36
46
|
const loading = ref(false)
|
|
47
|
+
/**
|
|
48
|
+
* Account-tier document fragments only: the workspace whose stored
|
|
49
|
+
* document-source connection is used to fetch/refresh the page (credentials are
|
|
50
|
+
* per-workspace). Set by the caller; ignored at the workspace scope (the owner
|
|
51
|
+
* board is used directly).
|
|
52
|
+
*/
|
|
53
|
+
const viaWorkspaceId = ref<string | undefined>(undefined)
|
|
37
54
|
|
|
38
55
|
const builtinCount = computed(() => resolved.value.filter((f) => f.tier === 'builtin').length)
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
function requireOwnerId(): string {
|
|
58
|
+
const id = resolveOwnerId()
|
|
59
|
+
if (!id) throw new Error('No fragment-library owner')
|
|
60
|
+
return id
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Probe the feature + load this owner's tier, sources and (ws) resolved catalog. */
|
|
41
64
|
async function probe() {
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
const id = resolveOwnerId()
|
|
66
|
+
if (!id) return
|
|
44
67
|
try {
|
|
45
68
|
const [tier, srcs, merged] = await Promise.all([
|
|
46
|
-
api.listFragments(
|
|
47
|
-
api.listFragmentSources(
|
|
48
|
-
api.getResolvedFragments(id),
|
|
69
|
+
api.listFragments(kind, id),
|
|
70
|
+
api.listFragmentSources(kind, id).catch(() => [] as FragmentSource[]),
|
|
71
|
+
hasResolved ? api.getResolvedFragments(id) : Promise.resolve([] as ResolvedFragment[]),
|
|
49
72
|
])
|
|
50
73
|
fragments.value = tier
|
|
51
74
|
sources.value = srcs
|
|
@@ -60,13 +83,14 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
async function refreshResolved() {
|
|
63
|
-
|
|
86
|
+
if (!hasResolved) return
|
|
87
|
+
resolved.value = await api.getResolvedFragments(requireOwnerId())
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
async function create(input: CreatePromptFragmentInput) {
|
|
67
91
|
loading.value = true
|
|
68
92
|
try {
|
|
69
|
-
await api.createFragment(
|
|
93
|
+
await api.createFragment(kind, requireOwnerId(), input)
|
|
70
94
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
71
95
|
} finally {
|
|
72
96
|
loading.value = false
|
|
@@ -74,7 +98,7 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
async function update(fragmentId: string, patch: UpdatePromptFragmentInput) {
|
|
77
|
-
await api.updateFragment(
|
|
101
|
+
await api.updateFragment(kind, requireOwnerId(), fragmentId, patch)
|
|
78
102
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
79
103
|
}
|
|
80
104
|
|
|
@@ -82,7 +106,10 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
82
106
|
async function createDocumentFragment(input: CreateDocumentFragmentInput) {
|
|
83
107
|
loading.value = true
|
|
84
108
|
try {
|
|
85
|
-
await api.createDocumentFragment(
|
|
109
|
+
await api.createDocumentFragment(kind, requireOwnerId(), {
|
|
110
|
+
...input,
|
|
111
|
+
...(viaWorkspaceId.value ? { viaWorkspaceId: viaWorkspaceId.value } : {}),
|
|
112
|
+
})
|
|
86
113
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
87
114
|
} finally {
|
|
88
115
|
loading.value = false
|
|
@@ -93,31 +120,31 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
93
120
|
async function refreshDocumentFragment(fragmentId: string) {
|
|
94
121
|
loading.value = true
|
|
95
122
|
try {
|
|
96
|
-
await api.refreshFragment(
|
|
123
|
+
await api.refreshFragment(kind, requireOwnerId(), fragmentId, viaWorkspaceId.value)
|
|
97
124
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
98
125
|
} finally {
|
|
99
126
|
loading.value = false
|
|
100
127
|
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
|
-
/** Tombstone a fragment at
|
|
130
|
+
/** Tombstone a fragment at this tier (suppresses an inherited one). */
|
|
104
131
|
async function remove(fragmentId: string) {
|
|
105
|
-
await api.deleteFragment(
|
|
132
|
+
await api.deleteFragment(kind, requireOwnerId(), fragmentId)
|
|
106
133
|
await Promise.all([reloadTier(), refreshResolved()])
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
async function reloadTier() {
|
|
110
|
-
fragments.value = await api.listFragments(
|
|
137
|
+
fragments.value = await api.listFragments(kind, requireOwnerId())
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
async function linkSource(input: LinkFragmentSourceInput) {
|
|
114
|
-
const source = await api.linkFragmentSource(
|
|
141
|
+
const source = await api.linkFragmentSource(kind, requireOwnerId(), input)
|
|
115
142
|
sources.value = [source, ...sources.value]
|
|
116
143
|
return source
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
async function unlinkSource(sourceId: string) {
|
|
120
|
-
await api.unlinkFragmentSource(
|
|
147
|
+
await api.unlinkFragmentSource(kind, requireOwnerId(), sourceId)
|
|
121
148
|
sources.value = sources.value.filter((s) => s.id !== sourceId)
|
|
122
149
|
await refreshResolved()
|
|
123
150
|
}
|
|
@@ -126,7 +153,7 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
126
153
|
async function syncSource(sourceId: string) {
|
|
127
154
|
loading.value = true
|
|
128
155
|
try {
|
|
129
|
-
const result = await api.syncFragmentSource(
|
|
156
|
+
const result = await api.syncFragmentSource(kind, requireOwnerId(), sourceId)
|
|
130
157
|
delete sourceChanges.value[sourceId]
|
|
131
158
|
await Promise.all([reloadSources(), refreshResolved()])
|
|
132
159
|
return result
|
|
@@ -137,7 +164,7 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
137
164
|
|
|
138
165
|
/** Cheap "check for changes" for a source; caches the changed count. */
|
|
139
166
|
async function checkSource(sourceId: string) {
|
|
140
|
-
const status = await api.fragmentSourceStatus(
|
|
167
|
+
const status = await api.fragmentSourceStatus(kind, requireOwnerId(), sourceId)
|
|
141
168
|
sourceChanges.value = {
|
|
142
169
|
...sourceChanges.value,
|
|
143
170
|
[sourceId]: status.changed ? status.changedCount : 0,
|
|
@@ -146,16 +173,19 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
146
173
|
}
|
|
147
174
|
|
|
148
175
|
async function reloadSources() {
|
|
149
|
-
sources.value = await api.listFragmentSources(
|
|
176
|
+
sources.value = await api.listFragmentSources(kind, requireOwnerId())
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
return {
|
|
180
|
+
kind,
|
|
181
|
+
hasResolved,
|
|
153
182
|
available,
|
|
154
183
|
fragments,
|
|
155
184
|
resolved,
|
|
156
185
|
sources,
|
|
157
186
|
sourceChanges,
|
|
158
187
|
loading,
|
|
188
|
+
viaWorkspaceId,
|
|
159
189
|
builtinCount,
|
|
160
190
|
probe,
|
|
161
191
|
refreshResolved,
|
|
@@ -169,4 +199,23 @@ export const useFragmentLibraryStore = defineStore('fragmentLibrary', () => {
|
|
|
169
199
|
syncSource,
|
|
170
200
|
checkSource,
|
|
171
201
|
}
|
|
172
|
-
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The workspace-tier library for the **active** board — a singleton that resolves
|
|
206
|
+
* the owner lazily, so it follows board switches and is shared by the navbar
|
|
207
|
+
* (SideBar/CommandBar probes) and the board fragment modal.
|
|
208
|
+
*/
|
|
209
|
+
export const useFragmentLibraryStore = defineStore('fragmentLibrary', () =>
|
|
210
|
+
fragmentLibrarySetup('workspace', () => useWorkspaceStore().workspaceId),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* An owner-keyed library store, used for the **account** tier (and reusable for any
|
|
215
|
+
* explicit owner). Keyed by `(kind, ownerId)` so each account gets isolated state.
|
|
216
|
+
*/
|
|
217
|
+
export function useFragmentLibrary(kind: FragmentOwnerKind, ownerId: string) {
|
|
218
|
+
return defineStore(`fragmentLibrary:${kind}:${ownerId}`, () =>
|
|
219
|
+
fragmentLibrarySetup(kind, () => ownerId),
|
|
220
|
+
)()
|
|
221
|
+
}
|
package/app/stores/ui.ts
CHANGED
|
@@ -96,6 +96,13 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
96
96
|
// `workspaceSettingsTab` lets other surfaces deep-link straight to a tab.
|
|
97
97
|
const workspaceSettingsOpen = ref(false)
|
|
98
98
|
const workspaceSettingsTab = ref('workspace')
|
|
99
|
+
// Account-settings modal: a single tabbed window for the per-account configuration —
|
|
100
|
+
// the team panel (members + roles + invitations + email sender + account API keys,
|
|
101
|
+
// `AccountTeamSettings`) and the account-tier prompt-fragment library. Account-scoped
|
|
102
|
+
// (distinct from workspace settings). `accountSettingsTab` lets other surfaces deep-link
|
|
103
|
+
// straight to a tab.
|
|
104
|
+
const accountSettingsOpen = ref(false)
|
|
105
|
+
const accountSettingsTab = ref('team')
|
|
99
106
|
// Observability integration: the post-release-health connection panel (Datadog
|
|
100
107
|
// today, pluggable). NB: distinct from `observabilityInstanceId` below, which is the
|
|
101
108
|
// LLM per-call observability panel.
|
|
@@ -380,6 +387,17 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
380
387
|
function setWorkspaceSettingsTab(tab: string) {
|
|
381
388
|
workspaceSettingsTab.value = tab
|
|
382
389
|
}
|
|
390
|
+
function openAccountSettings(tab = 'team') {
|
|
391
|
+
cameFromIntegrations.value = false
|
|
392
|
+
accountSettingsTab.value = tab
|
|
393
|
+
accountSettingsOpen.value = true
|
|
394
|
+
}
|
|
395
|
+
function closeAccountSettings() {
|
|
396
|
+
accountSettingsOpen.value = false
|
|
397
|
+
}
|
|
398
|
+
function setAccountSettingsTab(tab: string) {
|
|
399
|
+
accountSettingsTab.value = tab
|
|
400
|
+
}
|
|
383
401
|
function openObservabilityConnection() {
|
|
384
402
|
cameFromIntegrations.value = false
|
|
385
403
|
observabilityConnectionOpen.value = true
|
|
@@ -555,6 +573,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
555
573
|
cameFromIntegrations,
|
|
556
574
|
workspaceSettingsOpen,
|
|
557
575
|
workspaceSettingsTab,
|
|
576
|
+
accountSettingsOpen,
|
|
577
|
+
accountSettingsTab,
|
|
558
578
|
observabilityConnectionOpen,
|
|
559
579
|
providerConnectionKind,
|
|
560
580
|
modelConfigOpen,
|
|
@@ -617,6 +637,9 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
617
637
|
openWorkspaceSettings,
|
|
618
638
|
closeWorkspaceSettings,
|
|
619
639
|
setWorkspaceSettingsTab,
|
|
640
|
+
openAccountSettings,
|
|
641
|
+
closeAccountSettings,
|
|
642
|
+
setAccountSettingsTab,
|
|
620
643
|
openObservabilityConnection,
|
|
621
644
|
closeObservabilityConnection,
|
|
622
645
|
openProviderConnection,
|
package/app/types/domain.ts
CHANGED
|
@@ -512,6 +512,10 @@ export interface WorkspaceSettings {
|
|
|
512
512
|
storeAgentContext: boolean
|
|
513
513
|
/** Whether the Kaizen agent grades agent steps after each run. On by default. */
|
|
514
514
|
kaizenEnabled: boolean
|
|
515
|
+
/** Local mode only: dispatch container agents to the runner pool instead of host Docker. */
|
|
516
|
+
delegateAgentsToRunnerPool: boolean
|
|
517
|
+
/** Local mode only: provision Tester environments via the env provider instead of DinD. */
|
|
518
|
+
delegateTestEnvToProvider: boolean
|
|
515
519
|
/** Spend budget currency (ISO 4217). Null ⇒ the built-in default (`EUR`). */
|
|
516
520
|
spendCurrency: string | null
|
|
517
521
|
/** Monthly spend budget in `spendCurrency`. Null ⇒ the built-in default. */
|
|
@@ -526,6 +530,8 @@ export interface UpdateWorkspaceSettingsInput {
|
|
|
526
530
|
taskLimitPerType?: Partial<Record<CreateTaskType, number>> | null
|
|
527
531
|
storeAgentContext?: boolean
|
|
528
532
|
kaizenEnabled?: boolean
|
|
533
|
+
delegateAgentsToRunnerPool?: boolean
|
|
534
|
+
delegateTestEnvToProvider?: boolean
|
|
529
535
|
spendCurrency?: string | null
|
|
530
536
|
spendMonthlyLimit?: number | null
|
|
531
537
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|