@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.
- package/app/components/board/AddTaskModal.vue +45 -0
- package/app/components/layout/CommandBar.vue +4 -4
- package/app/components/layout/SideBar.vue +2 -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/composables/api/board.ts +1 -0
- package/app/composables/api/models.ts +0 -14
- package/app/composables/api/presets.ts +24 -1
- package/app/pages/index.vue +2 -2
- package/app/stores/board.ts +2 -0
- package/app/stores/modelPresets.ts +65 -0
- package/app/stores/ui.ts +8 -8
- package/app/stores/workspace.ts +2 -3
- package/app/types/domain.ts +5 -11
- package/app/types/model-presets.ts +33 -0
- package/package.json +1 -1
- package/app/components/settings/ModelDefaultsPanel.vue +0 -250
- package/app/stores/modelDefaults.ts +0 -76
|
@@ -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
|
|
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
|
}
|
package/app/pages/index.vue
CHANGED
|
@@ -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
|
|
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
|
-
<
|
|
125
|
+
<ModelConfigurationPanel />
|
|
126
126
|
<ServiceFragmentDefaultsPanel />
|
|
127
127
|
<LocalModelEndpointsPanel />
|
|
128
128
|
<OpenRouterCatalogPanel />
|
package/app/stores/board.ts
CHANGED
|
@@ -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
|
|
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
|
|
308
|
-
|
|
307
|
+
function openModelConfig() {
|
|
308
|
+
modelConfigOpen.value = true
|
|
309
309
|
}
|
|
310
|
-
function
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
439
|
+
openModelConfig,
|
|
440
|
+
closeModelConfig,
|
|
441
441
|
openServiceFragmentDefaults,
|
|
442
442
|
closeServiceFragmentDefaults,
|
|
443
443
|
openVendorCredentials,
|
package/app/stores/workspace.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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)
|
package/app/types/domain.ts
CHANGED
|
@@ -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
|
-
/**
|
|
389
|
-
|
|
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.
|
|
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
|
-
})
|