@cat-factory/app 0.26.7 → 0.28.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/layout/AccountDeploymentSettings.vue +236 -0
- package/app/components/layout/AccountTeamSettings.vue +6 -0
- package/app/components/layout/CommandBar.vue +8 -0
- package/app/components/layout/SideBar.vue +13 -0
- package/app/components/sandbox/SandboxPanel.vue +542 -0
- package/app/components/settings/ObservabilityConnectionPanel.vue +92 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +116 -1
- package/app/composables/api/accounts.ts +12 -0
- package/app/composables/api/releaseHealth.ts +17 -0
- package/app/composables/api/sandbox.ts +57 -0
- package/app/composables/useApi.ts +2 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/accountSettings.ts +49 -0
- package/app/stores/releaseHealth.ts +37 -0
- package/app/stores/sandbox.ts +174 -0
- package/app/stores/ui.ts +11 -0
- package/app/stores/workspaceSettings.ts +3 -0
- package/app/types/accountSettings.ts +39 -0
- package/app/types/domain.ts +9 -0
- package/app/types/incidentEnrichment.ts +28 -0
- package/app/types/sandbox.ts +183 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The latter three are body-only section components rendered in tabs here (no longer
|
|
9
9
|
// standalone modals).
|
|
10
10
|
import { reactive, ref, watch } from 'vue'
|
|
11
|
-
import type { CreateTaskType, TaskLimitMode } from '~/types/domain'
|
|
11
|
+
import type { CreateTaskType, TaskLimitMode, WorkspaceSettings } from '~/types/domain'
|
|
12
12
|
import MergeThresholdsPanel from '~/components/settings/MergeThresholdsPanel.vue'
|
|
13
13
|
import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
|
|
14
14
|
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
@@ -36,6 +36,7 @@ const tabs = [
|
|
|
36
36
|
icon: 'i-lucide-sliders-horizontal',
|
|
37
37
|
slot: 'workspace',
|
|
38
38
|
},
|
|
39
|
+
{ value: 'budget', label: 'Budget', icon: 'i-lucide-wallet', slot: 'budget' },
|
|
39
40
|
{ value: 'merge', label: 'Merge thresholds', icon: 'i-lucide-git-merge', slot: 'merge' },
|
|
40
41
|
{
|
|
41
42
|
value: 'tracker',
|
|
@@ -64,6 +65,10 @@ const draft = reactive({
|
|
|
64
65
|
taskLimitMode: 'off' as TaskLimitMode,
|
|
65
66
|
taskLimitShared: 5 as number,
|
|
66
67
|
perType: {} as Record<CreateTaskType, number>,
|
|
68
|
+
// Budget: empty string ⇒ "use the built-in default" (null on the wire).
|
|
69
|
+
spendCurrency: '',
|
|
70
|
+
spendMonthlyLimit: '',
|
|
71
|
+
spendModelPrices: '',
|
|
67
72
|
})
|
|
68
73
|
|
|
69
74
|
function hydrate() {
|
|
@@ -73,6 +78,9 @@ function hydrate() {
|
|
|
73
78
|
draft.taskLimitShared = s.taskLimitShared ?? 5
|
|
74
79
|
const pt = s.taskLimitPerType ?? {}
|
|
75
80
|
for (const t of TASK_TYPES) draft.perType[t] = pt[t] ?? 3
|
|
81
|
+
draft.spendCurrency = s.spendCurrency ?? ''
|
|
82
|
+
draft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
|
|
83
|
+
draft.spendModelPrices = s.spendModelPrices ? JSON.stringify(s.spendModelPrices, null, 2) : ''
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
watch(() => store.settings, hydrate, { immediate: true, deep: true })
|
|
@@ -109,6 +117,45 @@ async function save() {
|
|
|
109
117
|
saving.value = false
|
|
110
118
|
}
|
|
111
119
|
}
|
|
120
|
+
|
|
121
|
+
const savingBudget = ref(false)
|
|
122
|
+
|
|
123
|
+
async function saveBudget() {
|
|
124
|
+
// Parse the optional per-model price overrides JSON (blank ⇒ no overrides).
|
|
125
|
+
let prices: WorkspaceSettings['spendModelPrices'] = null
|
|
126
|
+
const raw = draft.spendModelPrices.trim()
|
|
127
|
+
if (raw) {
|
|
128
|
+
try {
|
|
129
|
+
prices = JSON.parse(raw)
|
|
130
|
+
} catch {
|
|
131
|
+
toast.add({
|
|
132
|
+
title: 'Per-model prices must be valid JSON',
|
|
133
|
+
icon: 'i-lucide-triangle-alert',
|
|
134
|
+
color: 'error',
|
|
135
|
+
})
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
savingBudget.value = true
|
|
140
|
+
try {
|
|
141
|
+
await store.update({
|
|
142
|
+
spendCurrency: draft.spendCurrency.trim() ? draft.spendCurrency.trim().toUpperCase() : null,
|
|
143
|
+
spendMonthlyLimit:
|
|
144
|
+
draft.spendMonthlyLimit.trim() === '' ? null : Number(draft.spendMonthlyLimit),
|
|
145
|
+
spendModelPrices: prices,
|
|
146
|
+
})
|
|
147
|
+
toast.add({ title: 'Budget saved', icon: 'i-lucide-check', color: 'success' })
|
|
148
|
+
} catch (e) {
|
|
149
|
+
toast.add({
|
|
150
|
+
title: 'Could not save budget',
|
|
151
|
+
description: e instanceof Error ? e.message : String(e),
|
|
152
|
+
icon: 'i-lucide-triangle-alert',
|
|
153
|
+
color: 'error',
|
|
154
|
+
})
|
|
155
|
+
} finally {
|
|
156
|
+
savingBudget.value = false
|
|
157
|
+
}
|
|
158
|
+
}
|
|
112
159
|
</script>
|
|
113
160
|
|
|
114
161
|
<template>
|
|
@@ -195,6 +242,74 @@ async function save() {
|
|
|
195
242
|
</div>
|
|
196
243
|
</template>
|
|
197
244
|
|
|
245
|
+
<!-- Budget -->
|
|
246
|
+
<template #budget>
|
|
247
|
+
<div class="space-y-6">
|
|
248
|
+
<section class="space-y-2">
|
|
249
|
+
<h3 class="text-sm font-semibold text-slate-200">Monthly spend budget</h3>
|
|
250
|
+
<p class="text-[11px] text-slate-400">
|
|
251
|
+
Token usage is metered per LLM call, priced, and gated by this budget — when
|
|
252
|
+
reached, runs in this workspace pause and the board shows a warning. Leave blank to
|
|
253
|
+
inherit the built-in default (~100 EUR/month).
|
|
254
|
+
</p>
|
|
255
|
+
<div class="grid grid-cols-2 gap-3">
|
|
256
|
+
<label class="block">
|
|
257
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
258
|
+
Monthly limit
|
|
259
|
+
</span>
|
|
260
|
+
<UInput
|
|
261
|
+
v-model="draft.spendMonthlyLimit"
|
|
262
|
+
type="number"
|
|
263
|
+
:min="0"
|
|
264
|
+
placeholder="Default"
|
|
265
|
+
size="sm"
|
|
266
|
+
/>
|
|
267
|
+
</label>
|
|
268
|
+
<label class="block">
|
|
269
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
270
|
+
Currency (ISO 4217)
|
|
271
|
+
</span>
|
|
272
|
+
<UInput
|
|
273
|
+
v-model="draft.spendCurrency"
|
|
274
|
+
placeholder="EUR"
|
|
275
|
+
maxlength="3"
|
|
276
|
+
size="sm"
|
|
277
|
+
class="uppercase"
|
|
278
|
+
/>
|
|
279
|
+
</label>
|
|
280
|
+
</div>
|
|
281
|
+
</section>
|
|
282
|
+
|
|
283
|
+
<section class="space-y-2">
|
|
284
|
+
<h3 class="text-sm font-semibold text-slate-200">Per-model price overrides</h3>
|
|
285
|
+
<p class="text-[11px] text-slate-400">
|
|
286
|
+
Optional. JSON object of <code>"provider:model"</code> (or a bare
|
|
287
|
+
<code>"provider"</code>) → <code>{ inputPerMillion, outputPerMillion }</code>,
|
|
288
|
+
overlaid on the built-in price table. Leave blank to use the defaults.
|
|
289
|
+
</p>
|
|
290
|
+
<UTextarea
|
|
291
|
+
v-model="draft.spendModelPrices"
|
|
292
|
+
:rows="6"
|
|
293
|
+
size="sm"
|
|
294
|
+
class="w-full font-mono text-[11px]"
|
|
295
|
+
placeholder='{"openai:gpt-4o":{"inputPerMillion":2.3,"outputPerMillion":9.2}}'
|
|
296
|
+
/>
|
|
297
|
+
</section>
|
|
298
|
+
|
|
299
|
+
<div class="flex justify-end">
|
|
300
|
+
<UButton
|
|
301
|
+
color="primary"
|
|
302
|
+
icon="i-lucide-save"
|
|
303
|
+
size="sm"
|
|
304
|
+
:loading="savingBudget"
|
|
305
|
+
@click="saveBudget"
|
|
306
|
+
>
|
|
307
|
+
Save budget
|
|
308
|
+
</UButton>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</template>
|
|
312
|
+
|
|
198
313
|
<!-- Merge thresholds -->
|
|
199
314
|
<template #merge>
|
|
200
315
|
<MergeThresholdsPanel />
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
EmailConnection,
|
|
8
8
|
UpdateAccountInput,
|
|
9
9
|
} from '~/types/domain'
|
|
10
|
+
import type { AccountSettingsView, UpdateAccountSettingsInput } from '~/types/accountSettings'
|
|
10
11
|
import type { ApiContext } from './context'
|
|
11
12
|
|
|
12
13
|
/** Account (tenancy) management: orgs, members, invitations + the email sender. */
|
|
@@ -77,5 +78,16 @@ export function accountsApi({ http }: ApiContext) {
|
|
|
77
78
|
method: 'POST',
|
|
78
79
|
body: { to },
|
|
79
80
|
}),
|
|
81
|
+
|
|
82
|
+
// Per-account deployment settings (admin only): integration secrets (Slack OAuth +
|
|
83
|
+
// web-search keys), sealed at rest. Read returns config + non-secret summary only.
|
|
84
|
+
getAccountSettings: (accountId: string) =>
|
|
85
|
+
http<AccountSettingsView>(`/accounts/${encodeURIComponent(accountId)}/settings`),
|
|
86
|
+
|
|
87
|
+
updateAccountSettings: (accountId: string, body: UpdateAccountSettingsInput) =>
|
|
88
|
+
http<AccountSettingsView>(`/accounts/${encodeURIComponent(accountId)}/settings`, {
|
|
89
|
+
method: 'PUT',
|
|
90
|
+
body,
|
|
91
|
+
}),
|
|
80
92
|
}
|
|
81
93
|
}
|
|
@@ -4,6 +4,10 @@ import type {
|
|
|
4
4
|
UpsertObservabilityConnectionInput,
|
|
5
5
|
UpsertReleaseHealthConfigInput,
|
|
6
6
|
} from '~/types/releaseHealth'
|
|
7
|
+
import type {
|
|
8
|
+
IncidentEnrichmentView,
|
|
9
|
+
UpsertIncidentEnrichmentInput,
|
|
10
|
+
} from '~/types/incidentEnrichment'
|
|
7
11
|
import type { ApiContext } from './context'
|
|
8
12
|
|
|
9
13
|
/** Post-release-health: the observability connection + per-block monitor/SLO mapping. */
|
|
@@ -40,5 +44,18 @@ export function releaseHealthApi({ http, ws }: ApiContext) {
|
|
|
40
44
|
http(`${ws(workspaceId)}/release-health-configs/${encodeURIComponent(blockId)}`, {
|
|
41
45
|
method: 'DELETE',
|
|
42
46
|
}),
|
|
47
|
+
|
|
48
|
+
// ---- Incident enrichment (PagerDuty + incident.io, write-only secrets) --
|
|
49
|
+
getIncidentEnrichment: (workspaceId: string) =>
|
|
50
|
+
http<IncidentEnrichmentView>(`${ws(workspaceId)}/incident-enrichment`),
|
|
51
|
+
|
|
52
|
+
setIncidentEnrichment: (workspaceId: string, body: UpsertIncidentEnrichmentInput) =>
|
|
53
|
+
http<IncidentEnrichmentView>(`${ws(workspaceId)}/incident-enrichment`, {
|
|
54
|
+
method: 'PUT',
|
|
55
|
+
body,
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
deleteIncidentEnrichment: (workspaceId: string) =>
|
|
59
|
+
http(`${ws(workspaceId)}/incident-enrichment`, { method: 'DELETE' }),
|
|
43
60
|
}
|
|
44
61
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CloneSandboxPromptInput,
|
|
3
|
+
CreateSandboxExperimentInput,
|
|
4
|
+
SandboxExperiment,
|
|
5
|
+
SandboxExperimentDetail,
|
|
6
|
+
SandboxFixture,
|
|
7
|
+
SandboxOverview,
|
|
8
|
+
SandboxPromptVersion,
|
|
9
|
+
SaveSandboxVersionInput,
|
|
10
|
+
} from '~/types/sandbox'
|
|
11
|
+
import type { ApiContext } from './context'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The Sandbox API (the parallel prompt/model testing surface): manage versioned prompt
|
|
15
|
+
* candidates + the fixture library, define experiments (prompt × model × fixture), and
|
|
16
|
+
* launch one to run + grade every cell. Opt-in: every endpoint 503s when the deployment
|
|
17
|
+
* hasn't wired the Sandbox (its dedicated DB / schema).
|
|
18
|
+
*/
|
|
19
|
+
export function sandboxApi({ http, ws }: ApiContext) {
|
|
20
|
+
const base = (workspaceId: string) => `${ws(workspaceId)}/sandbox`
|
|
21
|
+
return {
|
|
22
|
+
getSandboxOverview: (workspaceId: string) =>
|
|
23
|
+
http<SandboxOverview>(`${base(workspaceId)}/overview`),
|
|
24
|
+
|
|
25
|
+
// ---- prompt versions -------------------------------------------------
|
|
26
|
+
cloneSandboxPrompt: (workspaceId: string, body: CloneSandboxPromptInput) =>
|
|
27
|
+
http<SandboxPromptVersion>(`${base(workspaceId)}/prompts/clone`, { method: 'POST', body }),
|
|
28
|
+
saveSandboxVersion: (workspaceId: string, body: SaveSandboxVersionInput) =>
|
|
29
|
+
http<SandboxPromptVersion>(`${base(workspaceId)}/prompts`, { method: 'POST', body }),
|
|
30
|
+
setSandboxPromptLabels: (workspaceId: string, promptId: string, labels: string[]) =>
|
|
31
|
+
http<SandboxPromptVersion>(
|
|
32
|
+
`${base(workspaceId)}/prompts/${encodeURIComponent(promptId)}/labels`,
|
|
33
|
+
{ method: 'PATCH', body: { labels } },
|
|
34
|
+
),
|
|
35
|
+
archiveSandboxPrompt: (workspaceId: string, promptId: string) =>
|
|
36
|
+
http(`${base(workspaceId)}/prompts/${encodeURIComponent(promptId)}`, { method: 'DELETE' }),
|
|
37
|
+
|
|
38
|
+
// ---- fixtures --------------------------------------------------------
|
|
39
|
+
createSandboxFixture: (workspaceId: string, body: Partial<SandboxFixture>) =>
|
|
40
|
+
http<SandboxFixture>(`${base(workspaceId)}/fixtures`, { method: 'POST', body }),
|
|
41
|
+
deleteSandboxFixture: (workspaceId: string, fixtureId: string) =>
|
|
42
|
+
http(`${base(workspaceId)}/fixtures/${encodeURIComponent(fixtureId)}`, { method: 'DELETE' }),
|
|
43
|
+
|
|
44
|
+
// ---- experiments -----------------------------------------------------
|
|
45
|
+
createSandboxExperiment: (workspaceId: string, body: CreateSandboxExperimentInput) =>
|
|
46
|
+
http<SandboxExperiment>(`${base(workspaceId)}/experiments`, { method: 'POST', body }),
|
|
47
|
+
getSandboxExperiment: (workspaceId: string, experimentId: string) =>
|
|
48
|
+
http<SandboxExperimentDetail>(
|
|
49
|
+
`${base(workspaceId)}/experiments/${encodeURIComponent(experimentId)}`,
|
|
50
|
+
),
|
|
51
|
+
launchSandboxExperiment: (workspaceId: string, experimentId: string) =>
|
|
52
|
+
http<SandboxExperimentDetail>(
|
|
53
|
+
`${base(workspaceId)}/experiments/${encodeURIComponent(experimentId)}/launch`,
|
|
54
|
+
{ method: 'POST' },
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -15,6 +15,7 @@ import { presetsApi } from './api/presets'
|
|
|
15
15
|
import { providerConnectionsApi } from './api/providerConnections'
|
|
16
16
|
import { recurringApi } from './api/recurring'
|
|
17
17
|
import { releaseHealthApi } from './api/releaseHealth'
|
|
18
|
+
import { sandboxApi } from './api/sandbox'
|
|
18
19
|
import { reviewsApi } from './api/reviews'
|
|
19
20
|
import { slackApi } from './api/slack'
|
|
20
21
|
import { specApi } from './api/spec'
|
|
@@ -89,6 +90,7 @@ export function useApi() {
|
|
|
89
90
|
...providerConnectionsApi(ctx),
|
|
90
91
|
...releaseHealthApi(ctx),
|
|
91
92
|
...recurringApi(ctx),
|
|
93
|
+
...sandboxApi(ctx),
|
|
92
94
|
...githubApi(ctx),
|
|
93
95
|
...slackApi(ctx),
|
|
94
96
|
...bootstrapApi(ctx),
|
package/app/pages/index.vue
CHANGED
|
@@ -33,6 +33,7 @@ import ProviderConnectionPanel from '~/components/settings/ProviderConnectionPan
|
|
|
33
33
|
import ProviderConfigBanner from '~/components/layout/ProviderConfigBanner.vue'
|
|
34
34
|
import ModelConfigurationPanel from '~/components/settings/ModelConfigurationPanel.vue'
|
|
35
35
|
import LocalModelEndpointsPanel from '~/components/settings/LocalModelEndpointsPanel.vue'
|
|
36
|
+
import SandboxPanel from '~/components/sandbox/SandboxPanel.vue'
|
|
36
37
|
import UserSecretsSection from '~/components/settings/UserSecretsSection.vue'
|
|
37
38
|
import OpenRouterCatalogPanel from '~/components/settings/OpenRouterCatalogPanel.vue'
|
|
38
39
|
import VendorCredentialsModal from '~/components/providers/VendorCredentialsModal.vue'
|
|
@@ -189,6 +190,7 @@ watch(
|
|
|
189
190
|
<ProviderConnectionPanel />
|
|
190
191
|
<ModelConfigurationPanel />
|
|
191
192
|
<LocalModelEndpointsPanel />
|
|
193
|
+
<SandboxPanel />
|
|
192
194
|
<UserSecretsSection />
|
|
193
195
|
<OpenRouterCatalogPanel />
|
|
194
196
|
<VendorCredentialsModal />
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { AccountSettingsView, UpdateAccountSettingsInput } from '~/types/accountSettings'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-account deployment settings (admin only): the integration secrets — Slack app OAuth
|
|
7
|
+
* credentials + the web-search upstream keys — sealed at rest in the DB. Secrets are
|
|
8
|
+
* write-only: this store only ever holds the non-secret `summary` (which integrations are
|
|
9
|
+
* configured), never the values. `available` mirrors the backend opt-in: a 503 (no
|
|
10
|
+
* ENCRYPTION_KEY) hides the panel. Loaded on demand from the account-settings panel.
|
|
11
|
+
*/
|
|
12
|
+
export const useAccountSettingsStore = defineStore('accountSettings', () => {
|
|
13
|
+
const api = useApi()
|
|
14
|
+
|
|
15
|
+
const view = ref<AccountSettingsView | null>(null)
|
|
16
|
+
const loading = ref(false)
|
|
17
|
+
const available = ref<boolean | null>(null)
|
|
18
|
+
|
|
19
|
+
async function load(accountId: string) {
|
|
20
|
+
loading.value = true
|
|
21
|
+
try {
|
|
22
|
+
view.value = await api.getAccountSettings(accountId)
|
|
23
|
+
available.value = true
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// 503 ⇒ the settings store isn't wired (no ENCRYPTION_KEY) ⇒ hide the panel.
|
|
26
|
+
if (
|
|
27
|
+
e &&
|
|
28
|
+
typeof e === 'object' &&
|
|
29
|
+
'statusCode' in e &&
|
|
30
|
+
(e as { statusCode?: number }).statusCode === 503
|
|
31
|
+
) {
|
|
32
|
+
available.value = false
|
|
33
|
+
view.value = null
|
|
34
|
+
} else {
|
|
35
|
+
throw e
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
loading.value = false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function save(accountId: string, input: UpdateAccountSettingsInput) {
|
|
43
|
+
view.value = await api.updateAccountSettings(accountId, input)
|
|
44
|
+
available.value = true
|
|
45
|
+
return view.value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { view, loading, available, load, save }
|
|
49
|
+
})
|
|
@@ -6,6 +6,10 @@ import type {
|
|
|
6
6
|
UpsertObservabilityConnectionInput,
|
|
7
7
|
UpsertReleaseHealthConfigInput,
|
|
8
8
|
} from '~/types/releaseHealth'
|
|
9
|
+
import type {
|
|
10
|
+
IncidentEnrichmentView,
|
|
11
|
+
UpsertIncidentEnrichmentInput,
|
|
12
|
+
} from '~/types/incidentEnrichment'
|
|
9
13
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -23,6 +27,10 @@ export const useReleaseHealthStore = defineStore('releaseHealth', () => {
|
|
|
23
27
|
summary: null,
|
|
24
28
|
})
|
|
25
29
|
const configs = ref<ReleaseHealthConfig[]>([])
|
|
30
|
+
// Incident-enrichment (PagerDuty + incident.io) connection — write-only secrets, the
|
|
31
|
+
// store only ever holds the presence summary. Wired alongside observability.
|
|
32
|
+
const incident = ref<IncidentEnrichmentView>({ connected: false, summary: null })
|
|
33
|
+
const incidentAvailable = ref<boolean | null>(null)
|
|
26
34
|
const loading = ref(false)
|
|
27
35
|
// Mirrors the backend's opt-in gate (`OBSERVABILITY_ENABLED`): `null` until first
|
|
28
36
|
// probed, then `true`/`false`. The hub + inspector hide their observability entry
|
|
@@ -95,9 +103,35 @@ export const useReleaseHealthStore = defineStore('releaseHealth', () => {
|
|
|
95
103
|
configs.value = configs.value.filter((c) => c.blockId !== blockId)
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
/** Load the incident-enrichment connection (separate opt-in gate from observability). */
|
|
107
|
+
async function loadIncident() {
|
|
108
|
+
const ws = useWorkspaceStore()
|
|
109
|
+
try {
|
|
110
|
+
incident.value = await api.getIncidentEnrichment(ws.requireId())
|
|
111
|
+
incidentAvailable.value = true
|
|
112
|
+
} catch {
|
|
113
|
+
incidentAvailable.value = false
|
|
114
|
+
incident.value = { connected: false, summary: null }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function saveIncident(input: UpsertIncidentEnrichmentInput) {
|
|
119
|
+
const ws = useWorkspaceStore()
|
|
120
|
+
incident.value = await api.setIncidentEnrichment(ws.requireId(), input)
|
|
121
|
+
incidentAvailable.value = true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function removeIncident() {
|
|
125
|
+
const ws = useWorkspaceStore()
|
|
126
|
+
await api.deleteIncidentEnrichment(ws.requireId())
|
|
127
|
+
incident.value = { connected: false, summary: null }
|
|
128
|
+
}
|
|
129
|
+
|
|
98
130
|
return {
|
|
99
131
|
connection,
|
|
100
132
|
configs,
|
|
133
|
+
incident,
|
|
134
|
+
incidentAvailable,
|
|
101
135
|
loading,
|
|
102
136
|
available,
|
|
103
137
|
load,
|
|
@@ -107,5 +141,8 @@ export const useReleaseHealthStore = defineStore('releaseHealth', () => {
|
|
|
107
141
|
configForBlock,
|
|
108
142
|
saveConfig,
|
|
109
143
|
removeConfig,
|
|
144
|
+
loadIncident,
|
|
145
|
+
saveIncident,
|
|
146
|
+
removeIncident,
|
|
110
147
|
}
|
|
111
148
|
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { ModelOption } from '~/types/domain'
|
|
4
|
+
import type {
|
|
5
|
+
CreateSandboxExperimentInput,
|
|
6
|
+
SandboxAgentKindMeta,
|
|
7
|
+
SandboxExperiment,
|
|
8
|
+
SandboxExperimentDetail,
|
|
9
|
+
SandboxFixture,
|
|
10
|
+
SandboxOverview,
|
|
11
|
+
SandboxPromptVersion,
|
|
12
|
+
} from '~/types/sandbox'
|
|
13
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The Sandbox (parallel prompt/model testing surface). Loaded on demand when the panel
|
|
17
|
+
* opens (it's an opt-in, secondary surface, not part of the board snapshot): the testable
|
|
18
|
+
* agent-kind catalog, the shipped baselines + stored candidate prompt versions, the
|
|
19
|
+
* fixture library, and experiment definitions. Running an experiment grades every cell
|
|
20
|
+
* with a judge model; `launch` returns the full result grid.
|
|
21
|
+
*/
|
|
22
|
+
export const useSandboxStore = defineStore('sandbox', () => {
|
|
23
|
+
const api = useApi()
|
|
24
|
+
|
|
25
|
+
const available = ref(true)
|
|
26
|
+
const loading = ref(false)
|
|
27
|
+
const error = ref<string | null>(null)
|
|
28
|
+
|
|
29
|
+
const agentKinds = ref<SandboxAgentKindMeta[]>([])
|
|
30
|
+
const prompts = ref<SandboxPromptVersion[]>([])
|
|
31
|
+
const fixtures = ref<SandboxFixture[]>([])
|
|
32
|
+
const experiments = ref<SandboxExperiment[]>([])
|
|
33
|
+
const models = ref<ModelOption[]>([])
|
|
34
|
+
/** The matrix cell cap (from the backend overview, so the builder gates on the same limit). */
|
|
35
|
+
const maxCells = ref(100)
|
|
36
|
+
|
|
37
|
+
/** The currently-opened experiment's full detail (result grid), if any. */
|
|
38
|
+
const detail = ref<SandboxExperimentDetail | null>(null)
|
|
39
|
+
const launching = ref(false)
|
|
40
|
+
|
|
41
|
+
function hydrate(overview: SandboxOverview) {
|
|
42
|
+
agentKinds.value = overview.agentKinds
|
|
43
|
+
prompts.value = overview.prompts
|
|
44
|
+
fixtures.value = overview.fixtures
|
|
45
|
+
experiments.value = [...overview.experiments].sort((a, b) => b.createdAt - a.createdAt)
|
|
46
|
+
maxCells.value = overview.maxCells
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Patch one experiment into the list in place (newest-first), without a full reload. */
|
|
50
|
+
function upsertExperiment(experiment: SandboxExperiment) {
|
|
51
|
+
const next = experiments.value.filter((e) => e.id !== experiment.id)
|
|
52
|
+
next.push(experiment)
|
|
53
|
+
experiments.value = next.sort((a, b) => b.createdAt - a.createdAt)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Load the overview + the workspace model catalog. The 503 (feature off) is surfaced. */
|
|
57
|
+
async function load() {
|
|
58
|
+
const ws = useWorkspaceStore()
|
|
59
|
+
if (!ws.workspaceId) return
|
|
60
|
+
loading.value = true
|
|
61
|
+
error.value = null
|
|
62
|
+
try {
|
|
63
|
+
const [overview, modelList] = await Promise.all([
|
|
64
|
+
api.getSandboxOverview(ws.requireId()),
|
|
65
|
+
api.getWorkspaceModels(ws.requireId()),
|
|
66
|
+
])
|
|
67
|
+
hydrate(overview)
|
|
68
|
+
models.value = modelList
|
|
69
|
+
available.value = true
|
|
70
|
+
} catch (e) {
|
|
71
|
+
const status =
|
|
72
|
+
(e as { statusCode?: number; response?: { status?: number } })?.statusCode ??
|
|
73
|
+
(e as { response?: { status?: number } })?.response?.status
|
|
74
|
+
if (status === 503) {
|
|
75
|
+
available.value = false
|
|
76
|
+
} else {
|
|
77
|
+
error.value = e instanceof Error ? e.message : String(e)
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
loading.value = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Selectable models for the experiment picker (the backend computed `available`). */
|
|
85
|
+
const selectableModels = computed(() => models.value.filter((m) => m.available !== false))
|
|
86
|
+
|
|
87
|
+
/** Prompt versions for one agent kind (baselines first, then candidates). */
|
|
88
|
+
function promptsForKind(agentKind: string): SandboxPromptVersion[] {
|
|
89
|
+
return prompts.value.filter((p) => p.agentKind === agentKind)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Fixtures authored for one agent kind, filtered by the catalog's `fixtureKinds`. */
|
|
93
|
+
function fixturesForKind(agentKind: string): SandboxFixture[] {
|
|
94
|
+
const meta = agentKinds.value.find((k) => k.agentKind === agentKind)
|
|
95
|
+
if (!meta) return fixtures.value
|
|
96
|
+
// The backend catalog is the source of truth for the fixture↔kind mapping.
|
|
97
|
+
const wanted = meta.fixtureKinds
|
|
98
|
+
return fixtures.value.filter((f) => wanted.includes(f.kind))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function clonePrompt(agentKind: string, basePromptId: string | null, name?: string) {
|
|
102
|
+
const ws = useWorkspaceStore()
|
|
103
|
+
const created = await api.cloneSandboxPrompt(ws.requireId(), { agentKind, basePromptId, name })
|
|
104
|
+
await load()
|
|
105
|
+
return created
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function saveVersion(parentId: string, systemText: string) {
|
|
109
|
+
const ws = useWorkspaceStore()
|
|
110
|
+
const saved = await api.saveSandboxVersion(ws.requireId(), { parentId, systemText })
|
|
111
|
+
await load()
|
|
112
|
+
return saved
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function archivePrompt(promptId: string) {
|
|
116
|
+
const ws = useWorkspaceStore()
|
|
117
|
+
await api.archiveSandboxPrompt(ws.requireId(), promptId)
|
|
118
|
+
await load()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function createExperiment(input: CreateSandboxExperimentInput) {
|
|
122
|
+
const ws = useWorkspaceStore()
|
|
123
|
+
const created = await api.createSandboxExperiment(ws.requireId(), input)
|
|
124
|
+
await load()
|
|
125
|
+
return created
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function openExperiment(experimentId: string) {
|
|
129
|
+
const ws = useWorkspaceStore()
|
|
130
|
+
detail.value = await api.getSandboxExperiment(ws.requireId(), experimentId)
|
|
131
|
+
return detail.value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function launch(experimentId: string) {
|
|
135
|
+
const ws = useWorkspaceStore()
|
|
136
|
+
launching.value = true
|
|
137
|
+
try {
|
|
138
|
+
// `launch` returns the full graded grid AND the updated experiment, so patch both in
|
|
139
|
+
// place rather than calling `load()`: a transient failure in that follow-up fetch
|
|
140
|
+
// would otherwise set `error` and hide the freshly-returned result grid behind the
|
|
141
|
+
// error panel (and re-fetch the whole overview + model catalog for nothing).
|
|
142
|
+
const result = await api.launchSandboxExperiment(ws.requireId(), experimentId)
|
|
143
|
+
detail.value = result
|
|
144
|
+
upsertExperiment(result.experiment)
|
|
145
|
+
return result
|
|
146
|
+
} finally {
|
|
147
|
+
launching.value = false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
available,
|
|
153
|
+
loading,
|
|
154
|
+
error,
|
|
155
|
+
agentKinds,
|
|
156
|
+
prompts,
|
|
157
|
+
fixtures,
|
|
158
|
+
experiments,
|
|
159
|
+
models,
|
|
160
|
+
maxCells,
|
|
161
|
+
selectableModels,
|
|
162
|
+
detail,
|
|
163
|
+
launching,
|
|
164
|
+
load,
|
|
165
|
+
promptsForKind,
|
|
166
|
+
fixturesForKind,
|
|
167
|
+
clonePrompt,
|
|
168
|
+
saveVersion,
|
|
169
|
+
archivePrompt,
|
|
170
|
+
createExperiment,
|
|
171
|
+
openExperiment,
|
|
172
|
+
launch,
|
|
173
|
+
}
|
|
174
|
+
})
|
package/app/stores/ui.ts
CHANGED
|
@@ -104,6 +104,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
104
104
|
const vendorCredentialsOpen = ref(false)
|
|
105
105
|
// Per-user settings panel: the signed-in user's own-machine local model runners.
|
|
106
106
|
const localModelsOpen = ref(false)
|
|
107
|
+
// The Sandbox (parallel prompt/model testing) surface — an opt-in, on-demand window.
|
|
108
|
+
const sandboxOpen = ref(false)
|
|
107
109
|
const userSecretsOpen = ref(false)
|
|
108
110
|
// Per-workspace settings panel: the OpenRouter dynamic catalog (browse/enable gateway models).
|
|
109
111
|
const openRouterOpen = ref(false)
|
|
@@ -364,6 +366,12 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
364
366
|
function closeLocalModels() {
|
|
365
367
|
localModelsOpen.value = false
|
|
366
368
|
}
|
|
369
|
+
function openSandbox() {
|
|
370
|
+
sandboxOpen.value = true
|
|
371
|
+
}
|
|
372
|
+
function closeSandbox() {
|
|
373
|
+
sandboxOpen.value = false
|
|
374
|
+
}
|
|
367
375
|
function openUserSecrets() {
|
|
368
376
|
userSecretsOpen.value = true
|
|
369
377
|
}
|
|
@@ -464,6 +472,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
464
472
|
modelConfigOpen,
|
|
465
473
|
vendorCredentialsOpen,
|
|
466
474
|
localModelsOpen,
|
|
475
|
+
sandboxOpen,
|
|
467
476
|
userSecretsOpen,
|
|
468
477
|
openRouterOpen,
|
|
469
478
|
aiProviderSetupOpen,
|
|
@@ -528,6 +537,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
528
537
|
closeVendorCredentials,
|
|
529
538
|
openLocalModels,
|
|
530
539
|
closeLocalModels,
|
|
540
|
+
openSandbox,
|
|
541
|
+
closeSandbox,
|
|
531
542
|
openUserSecrets,
|
|
532
543
|
closeUserSecrets,
|
|
533
544
|
openOpenRouter,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Per-account (deployment-wide) integration settings. Mirrors
|
|
2
|
+
// `@cat-factory/contracts` accountSettings. Secrets are write-only — the read view
|
|
3
|
+
// returns only `config` + a non-secret presence `summary`.
|
|
4
|
+
|
|
5
|
+
export interface SlackOAuthSecret {
|
|
6
|
+
clientId: string
|
|
7
|
+
clientSecret: string
|
|
8
|
+
redirectUrl: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WebSearchSecret {
|
|
12
|
+
braveApiKey?: string
|
|
13
|
+
searxngUrl?: string
|
|
14
|
+
searxngApiKey?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Non-secret per-account config (empty today; reserved for forward-compatible tuning). */
|
|
18
|
+
export type AccountSettingsConfig = Record<string, never>
|
|
19
|
+
|
|
20
|
+
export interface AccountSettingsSummary {
|
|
21
|
+
slackOAuthConfigured: boolean
|
|
22
|
+
webSearch: 'brave' | 'searxng' | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AccountSettingsView {
|
|
26
|
+
config: AccountSettingsConfig
|
|
27
|
+
summary: AccountSettingsSummary
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Admin write. Each secrets group: omit ⇒ leave unchanged, `null` ⇒ clear, value ⇒ set.
|
|
32
|
+
*/
|
|
33
|
+
export interface UpdateAccountSettingsInput {
|
|
34
|
+
config?: AccountSettingsConfig
|
|
35
|
+
secrets?: {
|
|
36
|
+
slackOAuth?: SlackOAuthSecret | null
|
|
37
|
+
webSearch?: WebSearchSecret | null
|
|
38
|
+
}
|
|
39
|
+
}
|