@cat-factory/app 0.8.0 → 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.
@@ -39,6 +39,7 @@ export function boardApi({ http, ws }: ApiContext) {
39
39
  taskType?: CreateTaskType
40
40
  taskTypeFields?: TaskTypeFields
41
41
  mergePresetId?: string
42
+ modelPresetId?: string
42
43
  pipelineId?: string
43
44
  agentConfig?: Record<string, string>
44
45
  },
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  AddApiKeyInput,
3
3
  ApiKey,
4
- ModelDefaults,
5
4
  ModelOption,
6
5
  PersonalSubscriptionStatus,
7
6
  ServiceFragmentDefaults,
@@ -118,19 +117,6 @@ export function modelsApi({ http, ws }: ApiContext) {
118
117
  refreshOpenRouterCatalog: (workspaceId: string) =>
119
118
  http<OpenRouterRefreshResult>(`${ws(workspaceId)}/openrouter/refresh`, { method: 'POST' }),
120
119
 
121
- // ---- per-agent-kind default models (workspace routing overrides) ------
122
- // The workspace's map of agentKind → model id; a kind absent from the map
123
- // falls back to the deployment's env routing. `setModelDefaults` replaces the
124
- // whole map (the settings panel sends the full set on every change).
125
- getModelDefaults: (workspaceId: string) =>
126
- http<ModelDefaults>(`${ws(workspaceId)}/model-defaults`),
127
-
128
- setModelDefaults: (workspaceId: string, defaults: Record<string, string>) =>
129
- http<ModelDefaults>(`${ws(workspaceId)}/model-defaults`, {
130
- method: 'PUT',
131
- body: { defaults },
132
- }),
133
-
134
120
  // The workspace's default service-fragment selection (the fragment ids new
135
121
  // services inherit). `setServiceFragmentDefaults` replaces the whole list.
136
122
  getServiceFragmentDefaults: (workspaceId: string) =>
@@ -3,9 +3,14 @@ import type {
3
3
  MergeThresholdPreset,
4
4
  UpdateMergePresetInput,
5
5
  } from '~/types/merge'
6
+ import type {
7
+ CreateModelPresetInput,
8
+ ModelPreset,
9
+ UpdateModelPresetInput,
10
+ } from '~/types/model-presets'
6
11
  import type { ApiContext } from './context'
7
12
 
8
- /** The per-workspace merge-threshold preset library (per-task auto-merge policy). */
13
+ /** The per-workspace preset libraries: merge-threshold policy + model->agent mapping. */
9
14
  export function presetsApi({ http, ws }: ApiContext) {
10
15
  return {
11
16
  // ---- merge threshold presets (per-task auto-merge policy library) -----
@@ -25,5 +30,23 @@ export function presetsApi({ http, ws }: ApiContext) {
25
30
  http(`${ws(workspaceId)}/merge-presets/${encodeURIComponent(presetId)}`, {
26
31
  method: 'DELETE',
27
32
  }),
33
+
34
+ // ---- model presets (per-task model->agent mapping library) ------------
35
+ listModelPresets: (workspaceId: string) =>
36
+ http<ModelPreset[]>(`${ws(workspaceId)}/model-presets`),
37
+
38
+ createModelPreset: (workspaceId: string, body: CreateModelPresetInput) =>
39
+ http<ModelPreset>(`${ws(workspaceId)}/model-presets`, { method: 'POST', body }),
40
+
41
+ updateModelPreset: (workspaceId: string, presetId: string, body: UpdateModelPresetInput) =>
42
+ http<ModelPreset>(`${ws(workspaceId)}/model-presets/${encodeURIComponent(presetId)}`, {
43
+ method: 'PATCH',
44
+ body,
45
+ }),
46
+
47
+ deleteModelPreset: (workspaceId: string, presetId: string) =>
48
+ http(`${ws(workspaceId)}/model-presets/${encodeURIComponent(presetId)}`, {
49
+ method: 'DELETE',
50
+ }),
28
51
  }
29
52
  }
@@ -29,7 +29,7 @@ import MergeThresholdsPanel from '~/components/settings/MergeThresholdsPanel.vue
29
29
  import IssueTrackerWritebackPanel from '~/components/settings/IssueTrackerWritebackPanel.vue'
30
30
  import WorkspaceSettingsPanel from '~/components/settings/WorkspaceSettingsPanel.vue'
31
31
  import DatadogPanel from '~/components/settings/DatadogPanel.vue'
32
- import ModelDefaultsPanel from '~/components/settings/ModelDefaultsPanel.vue'
32
+ import ModelConfigurationPanel from '~/components/settings/ModelConfigurationPanel.vue'
33
33
  import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
34
34
  import LocalModelEndpointsPanel from '~/components/settings/LocalModelEndpointsPanel.vue'
35
35
  import OpenRouterCatalogPanel from '~/components/settings/OpenRouterCatalogPanel.vue'
@@ -122,7 +122,7 @@ watch(
122
122
  <IssueTrackerWritebackPanel />
123
123
  <WorkspaceSettingsPanel />
124
124
  <DatadogPanel />
125
- <ModelDefaultsPanel />
125
+ <ModelConfigurationPanel />
126
126
  <ServiceFragmentDefaultsPanel />
127
127
  <LocalModelEndpointsPanel />
128
128
  <OpenRouterCatalogPanel />
@@ -77,6 +77,7 @@ export const useBoardStore = defineStore('board', () => {
77
77
  taskType?: CreateTaskType
78
78
  taskTypeFields?: TaskTypeFields
79
79
  mergePresetId?: string
80
+ modelPresetId?: string
80
81
  pipelineId?: string
81
82
  agentConfig?: Record<string, string>
82
83
  },
@@ -88,6 +89,7 @@ export const useBoardStore = defineStore('board', () => {
88
89
  ...(options?.taskType ? { taskType: options.taskType } : {}),
89
90
  ...(options?.taskTypeFields ? { taskTypeFields: options.taskTypeFields } : {}),
90
91
  ...(options?.mergePresetId ? { mergePresetId: options.mergePresetId } : {}),
92
+ ...(options?.modelPresetId ? { modelPresetId: options.modelPresetId } : {}),
91
93
  ...(options?.pipelineId ? { pipelineId: options.pipelineId } : {}),
92
94
  ...(options?.agentConfig ? { agentConfig: options.agentConfig } : {}),
93
95
  })
@@ -0,0 +1,65 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ CreateModelPresetInput,
5
+ ModelPreset,
6
+ UpdateModelPresetInput,
7
+ } from '~/types/model-presets'
8
+ import { useWorkspaceStore } from '~/stores/workspace'
9
+
10
+ /**
11
+ * The workspace's model presets — the library a task picks its model→agent mapping
12
+ * from (each preset is a base model applied to every agent kind plus per-kind
13
+ * overrides). Hydrated from the workspace snapshot; managed via the Model Configuration
14
+ * settings screen. The backend always keeps at least one default preset (the built-in
15
+ * "Kimi K2.7", everything Kimi).
16
+ */
17
+ export const useModelPresetsStore = defineStore('modelPresets', () => {
18
+ const api = useApi()
19
+
20
+ const presets = ref<ModelPreset[]>([])
21
+
22
+ function hydrate(list: ModelPreset[]) {
23
+ presets.value = [...list].sort((a, b) => a.createdAt - b.createdAt)
24
+ }
25
+
26
+ /** The workspace default (fallback for a task that picks none). */
27
+ const defaultPreset = computed(() => presets.value.find((p) => p.isDefault) ?? null)
28
+
29
+ /** Resolve a task's effective preset by id, falling back to the default. */
30
+ function resolve(presetId: string | undefined): ModelPreset | null {
31
+ if (presetId) {
32
+ const picked = presets.value.find((p) => p.id === presetId)
33
+ if (picked) return picked
34
+ }
35
+ return defaultPreset.value
36
+ }
37
+
38
+ /** The model id a preset assigns to an agent kind (`overrides[kind] ?? baseModelId`). */
39
+ function modelForKind(preset: ModelPreset | null, agentKind: string): string | undefined {
40
+ if (!preset) return undefined
41
+ return preset.overrides[agentKind] ?? preset.baseModelId
42
+ }
43
+
44
+ async function create(input: CreateModelPresetInput) {
45
+ const ws = useWorkspaceStore()
46
+ const created = await api.createModelPreset(ws.requireId(), input)
47
+ await ws.refresh()
48
+ return created
49
+ }
50
+
51
+ async function update(presetId: string, patch: UpdateModelPresetInput) {
52
+ const ws = useWorkspaceStore()
53
+ const updated = await api.updateModelPreset(ws.requireId(), presetId, patch)
54
+ await ws.refresh()
55
+ return updated
56
+ }
57
+
58
+ async function remove(presetId: string) {
59
+ const ws = useWorkspaceStore()
60
+ await api.deleteModelPreset(ws.requireId(), presetId)
61
+ await ws.refresh()
62
+ }
63
+
64
+ return { presets, defaultPreset, resolve, modelForKind, hydrate, create, update, remove }
65
+ })
package/app/stores/ui.ts CHANGED
@@ -73,7 +73,7 @@ export const useUiStore = defineStore('ui', () => {
73
73
  // Workspace-settings panel: issue-tracker writeback toggles (comment on PR open,
74
74
  // close linked issue on merge).
75
75
  const issueWritebackOpen = ref(false)
76
- const modelDefaultsOpen = ref(false)
76
+ const modelConfigOpen = ref(false)
77
77
  // Workspace-settings panel: the default service-fragment selection new services inherit.
78
78
  const serviceFragmentDefaultsOpen = ref(false)
79
79
  // LLM-vendor subscription credentials (the token pool powering the Claude Code
@@ -304,11 +304,11 @@ export const useUiStore = defineStore('ui', () => {
304
304
  function closeDatadog() {
305
305
  datadogOpen.value = false
306
306
  }
307
- function openModelDefaults() {
308
- modelDefaultsOpen.value = true
307
+ function openModelConfig() {
308
+ modelConfigOpen.value = true
309
309
  }
310
- function closeModelDefaults() {
311
- modelDefaultsOpen.value = false
310
+ function closeModelConfig() {
311
+ modelConfigOpen.value = false
312
312
  }
313
313
  function openServiceFragmentDefaults() {
314
314
  serviceFragmentDefaultsOpen.value = true
@@ -380,7 +380,7 @@ export const useUiStore = defineStore('ui', () => {
380
380
  issueWritebackOpen,
381
381
  workspaceSettingsOpen,
382
382
  datadogOpen,
383
- modelDefaultsOpen,
383
+ modelConfigOpen,
384
384
  serviceFragmentDefaultsOpen,
385
385
  vendorCredentialsOpen,
386
386
  localModelsOpen,
@@ -436,8 +436,8 @@ export const useUiStore = defineStore('ui', () => {
436
436
  closeWorkspaceSettings,
437
437
  openDatadog,
438
438
  closeDatadog,
439
- openModelDefaults,
440
- closeModelDefaults,
439
+ openModelConfig,
440
+ closeModelConfig,
441
441
  openServiceFragmentDefaults,
442
442
  closeServiceFragmentDefaults,
443
443
  openVendorCredentials,
@@ -10,7 +10,7 @@ import { useNotificationsStore } from '~/stores/notifications'
10
10
  import { useMergePresetsStore } from '~/stores/mergePresets'
11
11
  import { useWorkspaceSettingsStore } from '~/stores/workspaceSettings'
12
12
  import { useAgentConfigStore } from '~/stores/agentConfig'
13
- import { useModelDefaultsStore } from '~/stores/modelDefaults'
13
+ import { useModelPresetsStore } from '~/stores/modelPresets'
14
14
  import { useServiceFragmentDefaultsStore } from '~/stores/serviceFragmentDefaults'
15
15
  import { useRecurringPipelinesStore } from '~/stores/recurringPipelines'
16
16
  import { useServicesStore } from '~/stores/services'
@@ -71,8 +71,7 @@ export const useWorkspaceStore = defineStore(
71
71
  useMergePresetsStore().hydrate(snapshot.mergePresets ?? [])
72
72
  useWorkspaceSettingsStore().hydrate(snapshot.settings)
73
73
  useAgentConfigStore().hydrate(snapshot.agentConfigCatalog ?? [])
74
- useModelDefaultsStore().hydrate(snapshot.modelDefaults?.defaults)
75
- useModelDefaultsStore().hydrateDeployment(snapshot.deploymentModelDefaults)
74
+ useModelPresetsStore().hydrate(snapshot.modelPresets ?? [])
76
75
  useServiceFragmentDefaultsStore().hydrate(snapshot.serviceFragmentDefaults?.fragmentIds)
77
76
  useRecurringPipelinesStore().hydrate(snapshot.recurringPipelines ?? [])
78
77
  useTrackerStore().hydrate(snapshot.trackerSettings)
@@ -21,6 +21,7 @@ import type { RequirementReview } from './requirements'
21
21
  import type { ConsensusSession, ConsensusStepConfig, StepGating, TaskEstimate } from './consensus'
22
22
  import type { ClarityReview } from './clarity'
23
23
  import type { MergeThresholdPreset } from './merge'
24
+ import type { ModelPreset } from './model-presets'
24
25
  import type { PipelineSchedule } from './recurring'
25
26
  import type { Service, WorkspaceMount } from './services'
26
27
  import type { TrackerSettings, WritebackOverride } from './tracker'
@@ -126,6 +127,8 @@ export interface Block {
126
127
  pullRequest?: PullRequestRef
127
128
  /** task-only: selected merge threshold preset id; absent = workspace default. */
128
129
  mergePresetId?: string
130
+ /** task-only: selected model preset id (which model each agent runs); absent = workspace default. */
131
+ modelPresetId?: string
129
132
  /** task-only: pinned default pipeline id picked at creation; absent = none. */
130
133
  pipelineId?: string
131
134
  /** task-only: agent-contributed config values (id→value), e.g. the Tester's environment. */
@@ -385,8 +388,8 @@ export interface WorkspaceSnapshot {
385
388
  mergePresets?: MergeThresholdPreset[]
386
389
  /** Agent config-contribution descriptors (the task-level fields the board renders). */
387
390
  agentConfigCatalog?: AgentConfigDescriptor[]
388
- /** Per-agent-kind default model overrides for this workspace (agentKind model id). */
389
- modelDefaults?: ModelDefaults
391
+ /** The workspace's model presets (the Model Configuration library + per-task picker). */
392
+ modelPresets?: ModelPreset[]
390
393
  /**
391
394
  * The deployment's env-routing defaults as `provider:model` refs: the model a
392
395
  * kind runs on when neither the task nor the workspace pins one. `default` is the
@@ -435,15 +438,6 @@ export interface UpdateWorkspaceSettingsInput {
435
438
  taskLimitPerType?: Partial<Record<CreateTaskType, number>> | null
436
439
  }
437
440
 
438
- /**
439
- * A workspace's per-agent-kind default model choice. Keys are agent kinds, values
440
- * are model catalog ids (`ModelOption.id`). A kind absent from the map falls back
441
- * to the deployment's env-configured routing. Mirrors `@cat-factory/contracts`.
442
- */
443
- export interface ModelDefaults {
444
- defaults: Record<string, string>
445
- }
446
-
447
441
  /**
448
442
  * A workspace's default service-fragment selection: the best-practice fragment ids
449
443
  * new services inherit onto their `serviceFragmentIds`. Mirrors `@cat-factory/contracts`.
@@ -0,0 +1,33 @@
1
+ // Model-preset shapes, mirroring `@cat-factory/contracts` (model-presets.ts). A preset
2
+ // is a named, per-workspace model->agent mapping: one base model applied to every
3
+ // agent kind plus per-kind overrides. A task selects one (Block.modelPresetId); none
4
+ // resolves to the workspace default preset.
5
+
6
+ /** A named, per-workspace model preset a task can select. */
7
+ export interface ModelPreset {
8
+ id: string
9
+ name: string
10
+ /** The model every agent kind defaults to under this preset (a catalog id). */
11
+ baseModelId: string
12
+ /** Per-agent-kind model overrides on top of the base (agent kind → model id). */
13
+ overrides: Record<string, string>
14
+ /** The workspace's fallback preset, used by tasks that pick none. */
15
+ isDefault: boolean
16
+ createdAt: number
17
+ }
18
+
19
+ /** Create a model preset. */
20
+ export interface CreateModelPresetInput {
21
+ name: string
22
+ baseModelId: string
23
+ overrides?: Record<string, string>
24
+ isDefault?: boolean
25
+ }
26
+
27
+ /** Patch a model preset (all fields optional; `overrides` replaces the map). */
28
+ export interface UpdateModelPresetInput {
29
+ name?: string
30
+ baseModelId?: string
31
+ overrides?: Record<string, string>
32
+ isDefault?: boolean
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -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
- })