@cat-factory/app 0.7.4 → 0.9.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/board/AddTaskModal.vue +45 -0
- package/app/components/layout/BoardToolbar.vue +0 -16
- package/app/components/layout/CommandBar.vue +4 -4
- package/app/components/layout/SideBar.vue +25 -2
- package/app/components/panels/inspector/TaskRunSettings.vue +59 -0
- package/app/components/pipeline/PipelineBuilder.vue +2 -2
- package/app/components/settings/ModelConfigurationPanel.vue +487 -0
- package/app/components/settings/OpenRouterCatalogPanel.vue +176 -0
- package/app/composables/api/board.ts +1 -0
- package/app/composables/api/models.ts +16 -12
- package/app/composables/api/presets.ts +24 -1
- package/app/pages/index.vue +8 -2
- package/app/stores/board.ts +2 -0
- package/app/stores/modelPresets.ts +65 -0
- package/app/stores/openrouter.ts +52 -0
- package/app/stores/ui.ts +19 -8
- package/app/stores/workspace.ts +2 -3
- package/app/types/domain.ts +5 -11
- package/app/types/model-presets.ts +33 -0
- package/app/types/models.ts +4 -4
- package/app/types/openrouter.ts +45 -0
- package/package.json +2 -2
- package/app/components/settings/ModelDefaultsPanel.vue +0 -250
- package/app/stores/modelDefaults.ts +0 -76
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// Workspace settings: the per-agent-kind default model overrides. For each agent
|
|
3
|
-
// kind you can pin which model its steps run on for this workspace; a kind left on
|
|
4
|
-
// the deployment default falls back to the env-configured routing (named here so
|
|
5
|
-
// you can see which model that actually is). A model pinned on an individual task
|
|
6
|
-
// still wins over these. Persisted via the modelDefaults store (the backend
|
|
7
|
-
// replaces the whole map on each change).
|
|
8
|
-
//
|
|
9
|
-
// Styled as a dark full-screen window like the agent-output review overlay
|
|
10
|
-
// (AgentStepDetail) rather than a light modal, so the text stays readable
|
|
11
|
-
// regardless of the OS colour-mode preference. The filter box narrows the list of
|
|
12
|
-
// AGENT KINDS (the catalog is long); each kind's model picker is a plain dropdown.
|
|
13
|
-
import { computed, ref, watch } from 'vue'
|
|
14
|
-
import { onKeyStroke } from '@vueuse/core'
|
|
15
|
-
import type { AgentKind } from '~/types/domain'
|
|
16
|
-
import { MODEL_CONFIGURABLE_SYSTEM_KINDS } from '~/utils/catalog'
|
|
17
|
-
import { contextLabel, costLabel, displayFlavor, isSelectable } from '~/stores/models'
|
|
18
|
-
|
|
19
|
-
const ui = useUiStore()
|
|
20
|
-
const models = useModelsStore()
|
|
21
|
-
const defaults = useModelDefaultsStore()
|
|
22
|
-
const agents = useAgentsStore()
|
|
23
|
-
const creds = useVendorCredentialsStore()
|
|
24
|
-
const workspace = useWorkspaceStore()
|
|
25
|
-
const toast = useToast()
|
|
26
|
-
|
|
27
|
-
const open = computed({
|
|
28
|
-
get: () => ui.modelDefaultsOpen,
|
|
29
|
-
set: (v: boolean) => (v ? ui.openModelDefaults() : ui.closeModelDefaults()),
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
const busy = ref<string | null>(null)
|
|
33
|
-
// Narrows the agent-kind rows below, for finding a kind fast in a long catalog.
|
|
34
|
-
const filter = ref('')
|
|
35
|
-
|
|
36
|
-
// The palette archetypes PLUS the engine-driven kinds that still run an LLM
|
|
37
|
-
// (spec-writer, merger, the fixers/resolver) — those aren't user-addable steps but their
|
|
38
|
-
// model is still worth pinning per workspace. The pure gates run no model, so they stay out.
|
|
39
|
-
const configurableKinds = computed(() => [...agents.archetypes, ...MODEL_CONFIGURABLE_SYSTEM_KINDS])
|
|
40
|
-
|
|
41
|
-
const filteredArchetypes = computed(() => {
|
|
42
|
-
const q = filter.value.trim().toLowerCase()
|
|
43
|
-
if (!q) return configurableKinds.value
|
|
44
|
-
return configurableKinds.value.filter(
|
|
45
|
-
(a) => a.label.toLowerCase().includes(q) || String(a.kind).toLowerCase().includes(q),
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
watch(open, (isOpen) => {
|
|
50
|
-
if (isOpen) {
|
|
51
|
-
filter.value = ''
|
|
52
|
-
void models.ensureLoaded(workspace.workspaceId ?? undefined)
|
|
53
|
-
if (workspace.workspaceId) void creds.load(workspace.workspaceId)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
onKeyStroke('Escape', () => {
|
|
58
|
-
if (open.value) open.value = false
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
/** The dropdown items for a kind's picker: the deployment-default reset plus the catalog. */
|
|
62
|
-
function menuFor(kind: AgentKind) {
|
|
63
|
-
const configured = creds.configuredVendors
|
|
64
|
-
return [
|
|
65
|
-
[
|
|
66
|
-
{
|
|
67
|
-
label: 'Deployment default',
|
|
68
|
-
icon: 'i-lucide-rotate-ccw',
|
|
69
|
-
onSelect: () => choose(kind, null),
|
|
70
|
-
},
|
|
71
|
-
...models.models
|
|
72
|
-
.filter((m) => isSelectable(m, configured))
|
|
73
|
-
.map((m) => {
|
|
74
|
-
const flavor = displayFlavor(m, configured)
|
|
75
|
-
const ctx = contextLabel(flavor.contextTokens)
|
|
76
|
-
// Show the model's list price too (already resolved from spend pricing on
|
|
77
|
-
// the catalog). `costLabel` folds the quota indicator into the cost string
|
|
78
|
-
// for quota-based models; fall back to a bare "quota" tag when a quota model
|
|
79
|
-
// carries no price.
|
|
80
|
-
const price = costLabel(flavor) ?? (flavor.quotaBased ? 'quota' : undefined)
|
|
81
|
-
const suffix = [flavor.providerLabel, ctx, price].filter(Boolean).join(' · ')
|
|
82
|
-
return {
|
|
83
|
-
label: `${m.label} · ${suffix}`,
|
|
84
|
-
icon: flavor.quotaBased ? 'i-lucide-infinity' : 'i-lucide-cpu',
|
|
85
|
-
onSelect: () => choose(kind, m.id),
|
|
86
|
-
}
|
|
87
|
-
}),
|
|
88
|
-
],
|
|
89
|
-
]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** The label shown on a kind's button: its pinned model, else the named deployment default. */
|
|
93
|
-
function buttonLabel(kind: AgentKind): string {
|
|
94
|
-
const pinned = defaults.forKind(kind)
|
|
95
|
-
if (pinned) {
|
|
96
|
-
const m = models.getModel(pinned)
|
|
97
|
-
// A pinned-but-uncatalogued id (e.g. a model whose provider key was since
|
|
98
|
-
// removed) shows the raw id rather than masquerading as a default.
|
|
99
|
-
if (!m) return pinned
|
|
100
|
-
return `${m.label} · ${displayFlavor(m, creds.configuredVendors).providerLabel}`
|
|
101
|
-
}
|
|
102
|
-
// No pin → name the env-routing model this kind actually falls back to.
|
|
103
|
-
const ref = defaults.deploymentRefForKind(kind)
|
|
104
|
-
const label = ref ? models.labelForRef(ref) : undefined
|
|
105
|
-
return label ? `${label} (default)` : 'Deployment default'
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function choose(kind: AgentKind, modelId: string | null) {
|
|
109
|
-
busy.value = kind
|
|
110
|
-
try {
|
|
111
|
-
await defaults.set(kind, modelId)
|
|
112
|
-
} catch (e) {
|
|
113
|
-
toast.add({
|
|
114
|
-
title: 'Could not save default model',
|
|
115
|
-
description: e instanceof Error ? e.message : String(e),
|
|
116
|
-
icon: 'i-lucide-triangle-alert',
|
|
117
|
-
color: 'error',
|
|
118
|
-
})
|
|
119
|
-
} finally {
|
|
120
|
-
busy.value = null
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
</script>
|
|
124
|
-
|
|
125
|
-
<template>
|
|
126
|
-
<Teleport to="body">
|
|
127
|
-
<Transition name="reader-fade">
|
|
128
|
-
<div
|
|
129
|
-
v-if="open"
|
|
130
|
-
class="fixed inset-0 z-50 flex flex-col bg-slate-950/96 backdrop-blur-sm"
|
|
131
|
-
role="dialog"
|
|
132
|
-
aria-modal="true"
|
|
133
|
-
>
|
|
134
|
-
<header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
|
|
135
|
-
<div
|
|
136
|
-
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500/15"
|
|
137
|
-
>
|
|
138
|
-
<UIcon name="i-lucide-cpu" class="h-5 w-5 text-indigo-300" />
|
|
139
|
-
</div>
|
|
140
|
-
<div class="min-w-0">
|
|
141
|
-
<h1 class="truncate text-base font-semibold text-white">Default models for agents</h1>
|
|
142
|
-
<p class="truncate text-xs text-slate-500">
|
|
143
|
-
Pin which model each agent kind runs on for this workspace.
|
|
144
|
-
</p>
|
|
145
|
-
</div>
|
|
146
|
-
<UButton
|
|
147
|
-
icon="i-lucide-x"
|
|
148
|
-
color="neutral"
|
|
149
|
-
variant="ghost"
|
|
150
|
-
size="sm"
|
|
151
|
-
class="ml-auto"
|
|
152
|
-
title="Close (Esc)"
|
|
153
|
-
@click="open = false"
|
|
154
|
-
/>
|
|
155
|
-
</header>
|
|
156
|
-
|
|
157
|
-
<div class="flex-1 overflow-auto px-6 py-6">
|
|
158
|
-
<div class="mx-auto max-w-3xl space-y-5">
|
|
159
|
-
<p class="text-sm leading-relaxed text-slate-400">
|
|
160
|
-
Pin which model each agent kind runs on for this workspace, e.g. a strong reasoning
|
|
161
|
-
model for the architect, a cheaper one for the documenter. A kind left on its
|
|
162
|
-
<span class="text-slate-300">deployment default</span> uses the server's configured
|
|
163
|
-
routing (named on the button). A model pinned on an individual task still overrides
|
|
164
|
-
these.
|
|
165
|
-
</p>
|
|
166
|
-
|
|
167
|
-
<UInput
|
|
168
|
-
v-model="filter"
|
|
169
|
-
icon="i-lucide-search"
|
|
170
|
-
size="sm"
|
|
171
|
-
placeholder="Filter agents…"
|
|
172
|
-
class="w-full"
|
|
173
|
-
>
|
|
174
|
-
<template v-if="filter" #trailing>
|
|
175
|
-
<UButton
|
|
176
|
-
icon="i-lucide-x"
|
|
177
|
-
color="neutral"
|
|
178
|
-
variant="link"
|
|
179
|
-
size="xs"
|
|
180
|
-
aria-label="Clear filter"
|
|
181
|
-
@click="filter = ''"
|
|
182
|
-
/>
|
|
183
|
-
</template>
|
|
184
|
-
</UInput>
|
|
185
|
-
|
|
186
|
-
<p v-if="models.models.length === 0" class="py-4 text-center text-sm text-slate-500">
|
|
187
|
-
Loading model catalog…
|
|
188
|
-
</p>
|
|
189
|
-
|
|
190
|
-
<div
|
|
191
|
-
v-else
|
|
192
|
-
class="divide-y divide-slate-800 rounded-xl border border-slate-800 bg-slate-900/50"
|
|
193
|
-
>
|
|
194
|
-
<div
|
|
195
|
-
v-for="a in filteredArchetypes"
|
|
196
|
-
:key="a.kind"
|
|
197
|
-
class="flex items-center gap-3 px-4 py-3"
|
|
198
|
-
>
|
|
199
|
-
<UIcon
|
|
200
|
-
:name="a.icon"
|
|
201
|
-
class="h-4 w-4 shrink-0"
|
|
202
|
-
:style="{ color: a.color }"
|
|
203
|
-
:title="a.description"
|
|
204
|
-
/>
|
|
205
|
-
<div class="min-w-0 flex-1" :title="a.description">
|
|
206
|
-
<p class="truncate text-sm text-slate-200">{{ a.label }}</p>
|
|
207
|
-
</div>
|
|
208
|
-
<!-- The menu content portals to <body>, where it would sit behind
|
|
209
|
-
this z-50 overlay (it carries no z-index of its own) and the
|
|
210
|
-
overlay would swallow the clicks — so lift it above. -->
|
|
211
|
-
<UDropdownMenu
|
|
212
|
-
:items="menuFor(a.kind)"
|
|
213
|
-
:ui="{ content: 'max-h-80 overflow-y-auto z-[60]' }"
|
|
214
|
-
>
|
|
215
|
-
<UButton
|
|
216
|
-
size="xs"
|
|
217
|
-
:color="defaults.forKind(a.kind) ? 'primary' : 'neutral'"
|
|
218
|
-
:variant="defaults.forKind(a.kind) ? 'subtle' : 'soft'"
|
|
219
|
-
trailing-icon="i-lucide-chevron-down"
|
|
220
|
-
:loading="busy === a.kind"
|
|
221
|
-
class="w-64 shrink-0 justify-between"
|
|
222
|
-
>
|
|
223
|
-
<span class="truncate">{{ buttonLabel(a.kind) }}</span>
|
|
224
|
-
</UButton>
|
|
225
|
-
</UDropdownMenu>
|
|
226
|
-
</div>
|
|
227
|
-
<p
|
|
228
|
-
v-if="filteredArchetypes.length === 0"
|
|
229
|
-
class="px-4 py-6 text-center text-sm text-slate-500"
|
|
230
|
-
>
|
|
231
|
-
No agents match "{{ filter }}".
|
|
232
|
-
</p>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
</Transition>
|
|
238
|
-
</Teleport>
|
|
239
|
-
</template>
|
|
240
|
-
|
|
241
|
-
<style scoped>
|
|
242
|
-
.reader-fade-enter-active,
|
|
243
|
-
.reader-fade-leave-active {
|
|
244
|
-
transition: opacity 0.18s ease;
|
|
245
|
-
}
|
|
246
|
-
.reader-fade-enter-from,
|
|
247
|
-
.reader-fade-leave-to {
|
|
248
|
-
opacity: 0;
|
|
249
|
-
}
|
|
250
|
-
</style>
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { defineStore } from 'pinia'
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The workspace's per-agent-kind default model overrides — the map an agent step
|
|
7
|
-
* resolves its model from when the task pins none (a block-pinned model still
|
|
8
|
-
* wins; a kind absent from the map falls back to the deployment's env routing).
|
|
9
|
-
* Hydrated from the workspace snapshot; edited via the Default-models settings
|
|
10
|
-
* panel, which replaces the whole map on save.
|
|
11
|
-
*/
|
|
12
|
-
export const useModelDefaultsStore = defineStore('modelDefaults', () => {
|
|
13
|
-
const api = useApi()
|
|
14
|
-
|
|
15
|
-
/** agentKind → model catalog id. */
|
|
16
|
-
const defaults = ref<Record<string, string>>({})
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The deployment's env-routing defaults as `provider:model` refs — what a kind
|
|
20
|
-
* runs on when neither the task nor this workspace pins a model. `default` is the
|
|
21
|
-
* global fallback; `byKind` carries kinds the operator routed specifically. Used
|
|
22
|
-
* only to NAME the fallback in the settings panel; it never overrides a pin.
|
|
23
|
-
*/
|
|
24
|
-
const deployment = ref<{ default: string; byKind: Record<string, string> }>({
|
|
25
|
-
default: '',
|
|
26
|
-
byKind: {},
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
function hydrate(map: Record<string, string> | undefined) {
|
|
30
|
-
defaults.value = { ...map }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function hydrateDeployment(
|
|
34
|
-
next: { default: string; byKind: Record<string, string> } | undefined,
|
|
35
|
-
) {
|
|
36
|
-
deployment.value = next
|
|
37
|
-
? { default: next.default, byKind: { ...next.byKind } }
|
|
38
|
-
: {
|
|
39
|
-
default: '',
|
|
40
|
-
byKind: {},
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The model id chosen for a kind, or undefined when it falls back to routing. */
|
|
45
|
-
function forKind(kind: string): string | undefined {
|
|
46
|
-
return defaults.value[kind]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The deployment-routing model ref a kind falls back to (`byKind[kind] ?? default`). */
|
|
50
|
-
function deploymentRefForKind(kind: string): string | undefined {
|
|
51
|
-
return deployment.value.byKind[kind] || deployment.value.default || undefined
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Set (or, with `null`, clear) the default model for a single agent kind, then
|
|
56
|
-
* persist the whole map. The backend replaces the stored set on every write.
|
|
57
|
-
*/
|
|
58
|
-
async function set(kind: string, modelId: string | null) {
|
|
59
|
-
const next = { ...defaults.value }
|
|
60
|
-
if (modelId) next[kind] = modelId
|
|
61
|
-
else delete next[kind]
|
|
62
|
-
const ws = useWorkspaceStore()
|
|
63
|
-
const saved = await api.setModelDefaults(ws.requireId(), next)
|
|
64
|
-
defaults.value = { ...saved.defaults }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
defaults,
|
|
69
|
-
deployment,
|
|
70
|
-
hydrate,
|
|
71
|
-
hydrateDeployment,
|
|
72
|
-
forKind,
|
|
73
|
-
deploymentRefForKind,
|
|
74
|
-
set,
|
|
75
|
-
}
|
|
76
|
-
})
|