@cat-factory/app 0.37.1 → 0.37.3
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/auth/LoginScreen.vue +2 -2
- package/app/components/board/nodes/BlockNode.vue +32 -13
- package/app/components/bootstrap/BootstrapModal.vue +10 -6
- package/app/components/documents/DocumentImportModal.vue +11 -7
- package/app/components/github/AddServiceFromRepoModal.vue +9 -5
- package/app/components/github/GitHubPanel.vue +8 -4
- package/app/components/kaizen/KaizenPanel.vue +7 -3
- package/app/components/layout/AccountTeamSettings.vue +2 -3
- package/app/components/layout/IntegrationsHub.vue +2 -0
- package/app/components/panels/ObservabilityPanel.vue +12 -7
- package/app/components/providers/VendorCredentialsModal.vue +10 -6
- package/app/components/sandbox/SandboxPanel.vue +30 -19
- package/app/components/settings/IssueTrackerPanel.vue +3 -1
- package/app/components/settings/LocalModeSettingsPanel.vue +7 -3
- package/app/components/settings/LocalModelEndpointsPanel.vue +7 -3
- package/app/components/settings/ModelConfigurationPanel.vue +12 -8
- package/app/components/settings/ObservabilityConnectionPanel.vue +16 -12
- package/app/components/settings/OpenRouterCatalogPanel.vue +14 -9
- package/app/components/settings/ProviderConnectionPanel.vue +4 -4
- package/app/components/settings/UserSecretsSection.vue +7 -3
- package/app/components/settings/WorkspaceSettingsPanel.vue +3 -1
- package/app/components/slack/SlackPanel.vue +2 -0
- package/app/composables/api/client.ts +11 -1
- package/app/composables/api/errors.spec.ts +53 -0
- package/app/composables/api/errors.ts +63 -0
- package/app/composables/useBlockQueries.ts +31 -9
- package/app/composables/usePipelineErrorToast.ts +5 -5
- package/app/composables/useSourceIntegration.ts +3 -3
- package/app/pages/index.vue +103 -51
- package/app/stores/board.spec.ts +30 -0
- package/app/stores/board.ts +27 -2
- package/app/stores/brainstorm.ts +11 -0
- package/app/stores/clarity.ts +11 -0
- package/app/stores/consensus.ts +7 -1
- package/app/stores/execution.spec.ts +43 -0
- package/app/stores/execution.ts +19 -0
- package/app/stores/github.ts +17 -0
- package/app/stores/personalSubscriptions.ts +2 -1
- package/app/stores/requirements.ts +12 -0
- package/app/stores/workspace.ts +17 -0
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
|
+
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
3
4
|
|
|
4
5
|
const auth = useAuthStore()
|
|
5
6
|
|
|
@@ -37,8 +38,7 @@ async function submitPassword() {
|
|
|
37
38
|
if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
|
|
38
39
|
} catch (e) {
|
|
39
40
|
error.value =
|
|
40
|
-
(e
|
|
41
|
-
'Sign-in failed. Check your details and try again.'
|
|
41
|
+
apiErrorEnvelope(e)?.message ?? 'Sign-in failed. Check your details and try again.'
|
|
42
42
|
} finally {
|
|
43
43
|
busy.value = false
|
|
44
44
|
}
|
|
@@ -35,8 +35,18 @@ const allTasks = computed(() => board.allTasksUnder(props.id))
|
|
|
35
35
|
const taskIds = computed(() => new Set(allTasks.value.map((t) => t.id)))
|
|
36
36
|
const taskCount = computed(() => allTasks.value.length)
|
|
37
37
|
const hasTasks = computed(() => taskCount.value > 0 || modules.value.length > 0)
|
|
38
|
-
|
|
39
|
-
const
|
|
38
|
+
// Single pass over the tasks for both rollups (vs. one filter each).
|
|
39
|
+
const taskStats = computed(() => {
|
|
40
|
+
let merged = 0
|
|
41
|
+
let prReady = 0
|
|
42
|
+
for (const t of allTasks.value) {
|
|
43
|
+
if (t.status === 'done') merged++
|
|
44
|
+
else if (t.status === 'pr_ready') prReady++
|
|
45
|
+
}
|
|
46
|
+
return { merged, prReady }
|
|
47
|
+
})
|
|
48
|
+
const mergedTasks = computed(() => taskStats.value.merged)
|
|
49
|
+
const prTasks = computed(() => taskStats.value.prReady)
|
|
40
50
|
const canvas = computed(() => board.containerSize(props.id))
|
|
41
51
|
|
|
42
52
|
// Frame status is derived from its tasks — services never reach "done".
|
|
@@ -60,10 +70,17 @@ const selected = computed(() => ui.selectedBlockId === props.id)
|
|
|
60
70
|
// kept (gated off) so the prior behaviour is one edit away if we want chips back.
|
|
61
71
|
const showExpanded = computed(() => true)
|
|
62
72
|
|
|
63
|
-
// Surface a pending decision from this frame OR any of its tasks
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
// Surface a pending decision from this frame OR any of its tasks (O(tasks) map
|
|
74
|
+
// lookups, not a scan of every open decision per frame).
|
|
75
|
+
const blockDecisions = computed(() => {
|
|
76
|
+
const byBlock = execution.decisionsByBlock
|
|
77
|
+
const out = [...(byBlock.get(props.id) ?? [])]
|
|
78
|
+
for (const id of taskIds.value) {
|
|
79
|
+
const list = byBlock.get(id)
|
|
80
|
+
if (list) out.push(...list)
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
})
|
|
67
84
|
|
|
68
85
|
function openFirstDecision() {
|
|
69
86
|
const d = blockDecisions.value[0]
|
|
@@ -74,13 +91,15 @@ function openFirstDecision() {
|
|
|
74
91
|
// iterative reviewer gate (requirements-review / clarity-review) that's mid-cycle
|
|
75
92
|
// (incorporating / re-reviewing in the driver), which is background work needing no human,
|
|
76
93
|
// so it stays off the frame's "Approval" badge.
|
|
77
|
-
const blockApprovals = computed(() =>
|
|
78
|
-
execution.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
const blockApprovals = computed(() => {
|
|
95
|
+
const byBlock = execution.approvalsByBlock
|
|
96
|
+
const candidates = [...(byBlock.get(props.id) ?? [])]
|
|
97
|
+
for (const id of taskIds.value) {
|
|
98
|
+
const list = byBlock.get(id)
|
|
99
|
+
if (list) candidates.push(...list)
|
|
100
|
+
}
|
|
101
|
+
return candidates.filter((a) => !reviews.isBackground(a.agentKind, a.blockId))
|
|
102
|
+
})
|
|
84
103
|
|
|
85
104
|
function openFirstApproval() {
|
|
86
105
|
const a = blockApprovals.value[0]
|
|
@@ -24,12 +24,16 @@ const open = computed({
|
|
|
24
24
|
|
|
25
25
|
// Load the workspace's reference architectures + recent jobs, plus (best-effort)
|
|
26
26
|
// the GitHub repos the user can access so the base form can pick from them.
|
|
27
|
-
watch(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
watch(
|
|
28
|
+
open,
|
|
29
|
+
(isOpen) => {
|
|
30
|
+
if (isOpen) {
|
|
31
|
+
void bootstrap.load()
|
|
32
|
+
void loadGitHubRepos()
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{ immediate: true },
|
|
36
|
+
)
|
|
33
37
|
|
|
34
38
|
async function loadGitHubRepos() {
|
|
35
39
|
try {
|
|
@@ -41,13 +41,17 @@ const sourceDocs = computed(() =>
|
|
|
41
41
|
source.value ? documents.documents.filter((d) => d.source === source.value) : [],
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
watch(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
44
|
+
watch(
|
|
45
|
+
open,
|
|
46
|
+
(isOpen) => {
|
|
47
|
+
if (isOpen) {
|
|
48
|
+
ref_.value = ''
|
|
49
|
+
source.value = ui.documentImport?.source ?? documents.connectedSources[0]?.source ?? undefined
|
|
50
|
+
documents.loadDocuments().catch(() => {})
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{ immediate: true },
|
|
54
|
+
)
|
|
51
55
|
|
|
52
56
|
async function doImport() {
|
|
53
57
|
const value = ref_.value.trim()
|
|
@@ -41,11 +41,15 @@ async function loadRepos() {
|
|
|
41
41
|
|
|
42
42
|
// On open: ensure we know the connection + which repos the App can access, and
|
|
43
43
|
// the workspace's already-tracked repos (to flag ones already on the board).
|
|
44
|
-
watch(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
watch(
|
|
45
|
+
open,
|
|
46
|
+
(isOpen) => {
|
|
47
|
+
if (!isOpen) return
|
|
48
|
+
resetSelection()
|
|
49
|
+
void loadRepos()
|
|
50
|
+
},
|
|
51
|
+
{ immediate: true },
|
|
52
|
+
)
|
|
49
53
|
|
|
50
54
|
// If the user connects from inside the modal (the not-connected prompt), pull the
|
|
51
55
|
// repo list as soon as the connection is bound.
|
|
@@ -26,10 +26,14 @@ const back = useIntegrationBack(open)
|
|
|
26
26
|
|
|
27
27
|
// On open: refresh projections when connected. The not-connected state renders
|
|
28
28
|
// <GitHubConnect>, which discovers and links installations on its own.
|
|
29
|
-
watch(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
watch(
|
|
30
|
+
open,
|
|
31
|
+
(isOpen) => {
|
|
32
|
+
if (!isOpen) return
|
|
33
|
+
if (github.connected) void github.load()
|
|
34
|
+
},
|
|
35
|
+
{ immediate: true },
|
|
36
|
+
)
|
|
33
37
|
|
|
34
38
|
function notifyError(title: string, e: unknown) {
|
|
35
39
|
toast.add({
|
|
@@ -12,9 +12,13 @@ const kaizen = useKaizenStore()
|
|
|
12
12
|
|
|
13
13
|
const open = computed(() => ui.kaizenScreenOpen)
|
|
14
14
|
|
|
15
|
-
watch(
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
watch(
|
|
16
|
+
open,
|
|
17
|
+
(isOpen) => {
|
|
18
|
+
if (isOpen) void kaizen.loadOverview()
|
|
19
|
+
},
|
|
20
|
+
{ immediate: true },
|
|
21
|
+
)
|
|
18
22
|
|
|
19
23
|
function close() {
|
|
20
24
|
ui.closeKaizen()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
3
4
|
import type { AccountRole } from '~/types/domain'
|
|
4
5
|
import AccountDeploymentSettings from '~/components/layout/AccountDeploymentSettings.vue'
|
|
5
6
|
|
|
@@ -39,9 +40,7 @@ async function updateMemberRoles(userId: string, roles: AccountRole[]) {
|
|
|
39
40
|
function notifyError(title: string, e: unknown) {
|
|
40
41
|
toast.add({
|
|
41
42
|
title,
|
|
42
|
-
description:
|
|
43
|
-
(e as { data?: { error?: { message?: string } } })?.data?.error?.message ??
|
|
44
|
-
(e instanceof Error ? e.message : String(e)),
|
|
43
|
+
description: apiErrorEnvelope(e)?.message ?? (e instanceof Error ? e.message : String(e)),
|
|
45
44
|
icon: 'i-lucide-triangle-alert',
|
|
46
45
|
color: 'error',
|
|
47
46
|
})
|
|
@@ -44,13 +44,18 @@ const contextLoading = computed(
|
|
|
44
44
|
|
|
45
45
|
// Load (and refresh) whenever a different run's panel opens. Reset to the calls view
|
|
46
46
|
// and load both the calls and the provided-context snapshots.
|
|
47
|
-
watch(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
watch(
|
|
48
|
+
executionId,
|
|
49
|
+
(id) => {
|
|
50
|
+
if (id) {
|
|
51
|
+
view.value = 'calls'
|
|
52
|
+
void observability.load(id)
|
|
53
|
+
void observability.loadContext(id)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
// Lazy v-if mount: the panel mounts with executionId already set, so load immediately.
|
|
57
|
+
{ immediate: true },
|
|
58
|
+
)
|
|
54
59
|
|
|
55
60
|
const expandedCtx = reactive<Record<string, boolean>>({})
|
|
56
61
|
function toggleCtx(s: AgentContextSnapshot) {
|
|
@@ -52,12 +52,16 @@ const label = ref('')
|
|
|
52
52
|
const token = ref('')
|
|
53
53
|
const busy = ref(false)
|
|
54
54
|
|
|
55
|
-
watch(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
watch(
|
|
56
|
+
open,
|
|
57
|
+
(isOpen) => {
|
|
58
|
+
if (!isOpen) return
|
|
59
|
+
// Honour a deep-linked tab each time the modal opens (e.g. "My subscriptions" → personal).
|
|
60
|
+
activeTab.value = ui.vendorCredentialsTab
|
|
61
|
+
if (workspace.workspaceId) void creds.load(workspace.workspaceId)
|
|
62
|
+
},
|
|
63
|
+
{ immediate: true },
|
|
64
|
+
)
|
|
61
65
|
|
|
62
66
|
/** Step-by-step instructions for the selected vendor. */
|
|
63
67
|
const steps = computed<string[]>(() => {
|
|
@@ -19,9 +19,13 @@ const open = computed({
|
|
|
19
19
|
|
|
20
20
|
const tab = ref<'experiments' | 'prompts' | 'fixtures'>('experiments')
|
|
21
21
|
|
|
22
|
-
watch(
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
watch(
|
|
23
|
+
open,
|
|
24
|
+
(isOpen) => {
|
|
25
|
+
if (isOpen) void store.load()
|
|
26
|
+
},
|
|
27
|
+
{ immediate: true },
|
|
28
|
+
)
|
|
25
29
|
|
|
26
30
|
// ---- experiment builder ----------------------------------------------------
|
|
27
31
|
const agentKind = ref('requirements-review')
|
|
@@ -107,6 +111,21 @@ const gradeByRun = computed(() => {
|
|
|
107
111
|
for (const g of store.detail?.grades ?? []) map.set(g.runId, g)
|
|
108
112
|
return map
|
|
109
113
|
})
|
|
114
|
+
// Fixture id → name, so a row resolves its fixture in O(1) instead of a .find scan.
|
|
115
|
+
const fixtureMap = computed(() => {
|
|
116
|
+
const map = new Map<string, string>()
|
|
117
|
+
for (const f of store.fixtures) map.set(f.id, f.name)
|
|
118
|
+
return map
|
|
119
|
+
})
|
|
120
|
+
// Pre-join each run with its grade + fixture name once, so the results table doesn't
|
|
121
|
+
// re-`.get()` the same grade four times (and `.find()` the fixture) per row on render.
|
|
122
|
+
const detailRows = computed(() =>
|
|
123
|
+
(store.detail?.runs ?? []).map((run) => ({
|
|
124
|
+
run,
|
|
125
|
+
grade: gradeByRun.value.get(run.id) ?? null,
|
|
126
|
+
fixtureName: fixtureMap.value.get(run.fixtureId) ?? run.fixtureId,
|
|
127
|
+
})),
|
|
128
|
+
)
|
|
110
129
|
const selectedRun = ref<SandboxRun | null>(null)
|
|
111
130
|
|
|
112
131
|
function scoreColor(score: number): string {
|
|
@@ -157,8 +176,6 @@ async function archive(prompt: SandboxPromptVersion) {
|
|
|
157
176
|
})
|
|
158
177
|
}
|
|
159
178
|
}
|
|
160
|
-
|
|
161
|
-
const fixtureName = (id: string) => store.fixtures.find((f) => f.id === id)?.name ?? id
|
|
162
179
|
</script>
|
|
163
180
|
|
|
164
181
|
<template>
|
|
@@ -348,7 +365,7 @@ const fixtureName = (id: string) => store.fixtures.find((f) => f.id === id)?.nam
|
|
|
348
365
|
</thead>
|
|
349
366
|
<tbody>
|
|
350
367
|
<tr
|
|
351
|
-
v-for="run in
|
|
368
|
+
v-for="{ run, grade, fixtureName } in detailRows"
|
|
352
369
|
:key="run.id"
|
|
353
370
|
class="cursor-pointer border-t border-slate-800 hover:bg-slate-800/40"
|
|
354
371
|
@click="selectedRun = run"
|
|
@@ -357,14 +374,14 @@ const fixtureName = (id: string) => store.fixtures.find((f) => f.id === id)?.nam
|
|
|
357
374
|
<td class="py-1 pr-2 font-mono text-[11px] text-slate-400">
|
|
358
375
|
{{ run.model }}
|
|
359
376
|
</td>
|
|
360
|
-
<td class="py-1 pr-2 text-slate-400">{{ fixtureName
|
|
377
|
+
<td class="py-1 pr-2 text-slate-400">{{ fixtureName }}</td>
|
|
361
378
|
<td class="py-1 pr-2">
|
|
362
379
|
<span
|
|
363
|
-
v-if="
|
|
364
|
-
:class="scoreColor(
|
|
380
|
+
v-if="grade"
|
|
381
|
+
:class="scoreColor(grade.weightedTotal)"
|
|
365
382
|
class="font-semibold"
|
|
366
383
|
>
|
|
367
|
-
{{
|
|
384
|
+
{{ grade.weightedTotal.toFixed(2) }}
|
|
368
385
|
</span>
|
|
369
386
|
<span v-else-if="run.status === 'failed'" class="text-rose-400"
|
|
370
387
|
>failed</span
|
|
@@ -373,16 +390,10 @@ const fixtureName = (id: string) => store.fixtures.find((f) => f.id === id)?.nam
|
|
|
373
390
|
</td>
|
|
374
391
|
<td class="py-1">
|
|
375
392
|
<span
|
|
376
|
-
v-if="
|
|
377
|
-
:class="
|
|
378
|
-
gradeByRun.get(run.id)!.objective!.pass
|
|
379
|
-
? 'text-emerald-400'
|
|
380
|
-
: 'text-amber-400'
|
|
381
|
-
"
|
|
393
|
+
v-if="grade?.objective"
|
|
394
|
+
:class="grade.objective.pass ? 'text-emerald-400' : 'text-amber-400'"
|
|
382
395
|
>
|
|
383
|
-
{{
|
|
384
|
-
gradeByRun.get(run.id)!.objective!.total
|
|
385
|
-
}}
|
|
396
|
+
{{ grade.objective.caught }}/{{ grade.objective.total }}
|
|
386
397
|
</span>
|
|
387
398
|
<span v-else class="text-slate-600">—</span>
|
|
388
399
|
</td>
|
|
@@ -41,7 +41,9 @@ onMounted(() => {
|
|
|
41
41
|
// probe on open if the navbar hasn't already, so the toggles below reflect reality.
|
|
42
42
|
if (tasks.available === null) void tasks.probe()
|
|
43
43
|
})
|
|
44
|
-
|
|
44
|
+
// `tracker.settings` is reassigned wholesale on hydrate/save, so a reference watch
|
|
45
|
+
// (no deep traversal) catches every change.
|
|
46
|
+
watch(() => tracker.settings, hydrate)
|
|
45
47
|
|
|
46
48
|
// Per-source live state (available = usable now; enabled = offered to the workspace).
|
|
47
49
|
const github = computed(() => tasks.descriptorFor('github'))
|
|
@@ -43,9 +43,13 @@ function syncDraft() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Load + hydrate the draft whenever the panel opens.
|
|
46
|
-
watch(
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
watch(
|
|
47
|
+
open,
|
|
48
|
+
(isOpen) => {
|
|
49
|
+
if (isOpen) void store.load().then(syncDraft)
|
|
50
|
+
},
|
|
51
|
+
{ immediate: true },
|
|
52
|
+
)
|
|
49
53
|
watch(() => store.settings, syncDraft)
|
|
50
54
|
|
|
51
55
|
async function save() {
|
|
@@ -21,9 +21,13 @@ const back = useIntegrationBack(open)
|
|
|
21
21
|
|
|
22
22
|
// Load the user's endpoints whenever the panel opens (loaded independently of the
|
|
23
23
|
// workspace snapshot, like personal subscriptions).
|
|
24
|
-
watch(
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
watch(
|
|
25
|
+
open,
|
|
26
|
+
(isOpen) => {
|
|
27
|
+
if (isOpen) void store.load()
|
|
28
|
+
},
|
|
29
|
+
{ immediate: true },
|
|
30
|
+
)
|
|
27
31
|
|
|
28
32
|
const RUNNERS: { value: LocalRunner; label: string }[] = (
|
|
29
33
|
Object.keys(LOCAL_RUNNER_LABELS) as LocalRunner[]
|
|
@@ -55,14 +55,18 @@ const filteredKinds = computed(() => {
|
|
|
55
55
|
)
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
watch(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
watch(
|
|
59
|
+
open,
|
|
60
|
+
(isOpen) => {
|
|
61
|
+
if (isOpen) {
|
|
62
|
+
editor.value = null
|
|
63
|
+
filter.value = ''
|
|
64
|
+
void models.ensureLoaded(workspace.workspaceId ?? undefined)
|
|
65
|
+
if (workspace.workspaceId) void creds.load(workspace.workspaceId)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{ immediate: true },
|
|
69
|
+
)
|
|
66
70
|
|
|
67
71
|
onKeyStroke('Escape', () => {
|
|
68
72
|
if (!open.value) return
|
|
@@ -42,18 +42,22 @@ function notifyError(title: string, e: unknown) {
|
|
|
42
42
|
})
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
watch(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
watch(
|
|
46
|
+
open,
|
|
47
|
+
async (isOpen) => {
|
|
48
|
+
if (!isOpen) return
|
|
49
|
+
try {
|
|
50
|
+
await store.ensureLoaded()
|
|
51
|
+
if (store.connection.provider) provider.value = store.connection.provider
|
|
52
|
+
const site = store.connection.summary?.site
|
|
53
|
+
if (site) datadog.site = site
|
|
54
|
+
await store.loadIncident()
|
|
55
|
+
} catch (e) {
|
|
56
|
+
notifyError('Could not load observability settings', e)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{ immediate: true },
|
|
60
|
+
)
|
|
57
61
|
|
|
58
62
|
async function saveIncident() {
|
|
59
63
|
incidentBusy.value = true
|
|
@@ -51,15 +51,20 @@ const connectingKey = ref(false)
|
|
|
51
51
|
|
|
52
52
|
// Load key state + persisted catalog whenever the panel opens; seed the tick selection,
|
|
53
53
|
// then auto-refresh the live catalog if a key is already connected (no extra click).
|
|
54
|
-
watch(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
watch(
|
|
55
|
+
open,
|
|
56
|
+
(isOpen) => {
|
|
57
|
+
// Lazy v-if mount runs this immediately (see below); guard still skips the closed case.
|
|
58
|
+
if (!isOpen || !workspace.workspaceId) return
|
|
59
|
+
const ws = workspace.workspaceId
|
|
60
|
+
void apiKeys.load(ws).catch(() => {})
|
|
61
|
+
void store.load(ws).then(() => {
|
|
62
|
+
selected.value = new Set(store.enabled.map((m) => m.id))
|
|
63
|
+
if (keyConnected.value && store.browse.length === 0) void refresh()
|
|
64
|
+
})
|
|
65
|
+
},
|
|
66
|
+
{ immediate: true },
|
|
67
|
+
)
|
|
63
68
|
|
|
64
69
|
// The list to show: the live browse list once refreshed, else the persisted enabled set.
|
|
65
70
|
const source = computed<OpenRouterModelMeta[]>(() =>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// see backend/docs/native-environment-adapter.md): a `secret` field → the write-only secret
|
|
8
8
|
// bundle, a non-secret field → providerConfig[key], a `baseUrl` field → baseUrl. A field
|
|
9
9
|
// with a `default` is optional — left blank it falls back to that default.
|
|
10
|
-
import { computed, ref, watch } from 'vue'
|
|
10
|
+
import { computed, ref, toRaw, watch } from 'vue'
|
|
11
11
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
12
12
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
13
13
|
import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
|
|
@@ -163,9 +163,9 @@ function buildManifestPayload(): {
|
|
|
163
163
|
const template = descriptor.value?.manifestTemplate
|
|
164
164
|
if (!template) return null
|
|
165
165
|
const base = descriptor.value?.savedManifest ?? template
|
|
166
|
-
// `base` is a Vue reactive proxy, which structuredClone refuses (DataCloneError).
|
|
167
|
-
//
|
|
168
|
-
const manifest: Record<string, unknown> =
|
|
166
|
+
// `base` is a Vue reactive proxy, which structuredClone refuses (DataCloneError). `toRaw`
|
|
167
|
+
// unwraps it to the underlying plain-JSON config so structuredClone can deep-clone it.
|
|
168
|
+
const manifest: Record<string, unknown> = structuredClone(toRaw(base))
|
|
169
169
|
const providerConfig: Record<string, unknown> = {
|
|
170
170
|
...(manifest.providerConfig as Record<string, unknown> | undefined),
|
|
171
171
|
}
|
|
@@ -40,9 +40,13 @@ function resetDraft() {
|
|
|
40
40
|
if (meta) for (const [k, v] of Object.entries(meta)) values.value[k] = v
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
watch(
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
watch(
|
|
44
|
+
open,
|
|
45
|
+
(isOpen) => {
|
|
46
|
+
if (isOpen) void store.load().then(resetDraft)
|
|
47
|
+
},
|
|
48
|
+
{ immediate: true },
|
|
49
|
+
)
|
|
46
50
|
watch(kind, resetDraft)
|
|
47
51
|
|
|
48
52
|
const secretField = computed<ProviderConfigField | undefined>(() =>
|
|
@@ -87,7 +87,9 @@ function hydrate() {
|
|
|
87
87
|
draft.spendMonthlyLimit = s.spendMonthlyLimit == null ? '' : String(s.spendMonthlyLimit)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// `store.settings` is always replaced wholesale (store hydrate/update reassign the ref),
|
|
91
|
+
// so tracking the object reference is enough — no deep per-field traversal needed.
|
|
92
|
+
watch(() => store.settings, hydrate, { immediate: true })
|
|
91
93
|
|
|
92
94
|
const saving = ref(false)
|
|
93
95
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type WretchInstance,
|
|
12
12
|
} from '@toad-contracts/frontend-http-client'
|
|
13
13
|
import wretch from 'wretch'
|
|
14
|
+
import { ApiError } from './errors'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The validated success-response body inferred from a route contract (every REST
|
|
@@ -75,7 +76,16 @@ export async function sendContract<T extends ApiContract>(
|
|
|
75
76
|
params: SendParams<T>,
|
|
76
77
|
): Promise<SuccessBodyOf<T>> {
|
|
77
78
|
const outcome = await sendByApiContract(client, contract, params)
|
|
78
|
-
if (outcome.error)
|
|
79
|
+
if (outcome.error) {
|
|
80
|
+
const error = outcome.error
|
|
81
|
+
// A contract-declared non-2xx is reported as a plain `{ statusCode, headers, body }`
|
|
82
|
+
// value (not an Error). Wrap it so call sites get `instanceof Error` + the server's
|
|
83
|
+
// message; anything already an Error (UnexpectedResponseError, request-validation
|
|
84
|
+
// SchemaValidationError, a network fault) is rethrown unchanged.
|
|
85
|
+
if (error instanceof Error) throw error
|
|
86
|
+
const { statusCode, body } = error as { statusCode: number; body: unknown }
|
|
87
|
+
throw new ApiError(statusCode, body)
|
|
88
|
+
}
|
|
79
89
|
return outcome.result!.body as SuccessBodyOf<T>
|
|
80
90
|
}
|
|
81
91
|
|