@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.
@@ -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
- })