@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
|
@@ -18,6 +18,7 @@ const board = useBoardStore()
|
|
|
18
18
|
const documents = useDocumentsStore()
|
|
19
19
|
const tasks = useTasksStore()
|
|
20
20
|
const mergePresets = useMergePresetsStore()
|
|
21
|
+
const modelPresets = useModelPresetsStore()
|
|
21
22
|
const pipelines = usePipelinesStore()
|
|
22
23
|
const agentConfig = useAgentConfigStore()
|
|
23
24
|
const toast = useToast()
|
|
@@ -94,6 +95,7 @@ const recurringFrameId = computed(() => {
|
|
|
94
95
|
// Run configuration picked up front. Empty string = use the default (workspace
|
|
95
96
|
// default merge preset / no pinned pipeline).
|
|
96
97
|
const mergePresetId = ref('')
|
|
98
|
+
const modelPresetId = ref('')
|
|
97
99
|
const pipelineId = ref('')
|
|
98
100
|
|
|
99
101
|
const presetMenu = computed(() => [
|
|
@@ -121,6 +123,32 @@ const selectedPresetLabel = computed(() => {
|
|
|
121
123
|
return mergePresets.presets.find((p) => p.id === mergePresetId.value)?.name ?? 'Workspace default'
|
|
122
124
|
})
|
|
123
125
|
|
|
126
|
+
// Model preset: which model each agent runs on. Empty = workspace default preset.
|
|
127
|
+
const modelPresetMenu = computed(() => [
|
|
128
|
+
[
|
|
129
|
+
{
|
|
130
|
+
label: modelPresets.defaultPreset
|
|
131
|
+
? `Default (${modelPresets.defaultPreset.name})`
|
|
132
|
+
: 'Workspace default',
|
|
133
|
+
icon: 'i-lucide-rotate-ccw',
|
|
134
|
+
onSelect: () => (modelPresetId.value = ''),
|
|
135
|
+
},
|
|
136
|
+
...modelPresets.presets.map((p) => ({
|
|
137
|
+
label: p.name,
|
|
138
|
+
icon: 'i-lucide-cpu',
|
|
139
|
+
onSelect: () => (modelPresetId.value = p.id),
|
|
140
|
+
})),
|
|
141
|
+
],
|
|
142
|
+
])
|
|
143
|
+
const selectedModelPresetLabel = computed(() => {
|
|
144
|
+
if (!modelPresetId.value) {
|
|
145
|
+
return modelPresets.defaultPreset
|
|
146
|
+
? `Default (${modelPresets.defaultPreset.name})`
|
|
147
|
+
: 'Workspace default'
|
|
148
|
+
}
|
|
149
|
+
return modelPresets.presets.find((p) => p.id === modelPresetId.value)?.name ?? 'Workspace default'
|
|
150
|
+
})
|
|
151
|
+
|
|
124
152
|
const pipelineMenu = computed(() => [
|
|
125
153
|
[
|
|
126
154
|
{
|
|
@@ -172,6 +200,7 @@ watch(open, (isOpen) => {
|
|
|
172
200
|
timeboxHours.value = undefined
|
|
173
201
|
docKind.value = ''
|
|
174
202
|
mergePresetId.value = ''
|
|
203
|
+
modelPresetId.value = ''
|
|
175
204
|
pipelineId.value = ''
|
|
176
205
|
agentConfigValues.value = {}
|
|
177
206
|
pendingContext.value = []
|
|
@@ -208,6 +237,7 @@ async function add() {
|
|
|
208
237
|
taskType: taskType.value as CreateTaskType,
|
|
209
238
|
...(typeFields ? { taskTypeFields: typeFields } : {}),
|
|
210
239
|
...(mergePresetId.value ? { mergePresetId: mergePresetId.value } : {}),
|
|
240
|
+
...(modelPresetId.value ? { modelPresetId: modelPresetId.value } : {}),
|
|
211
241
|
...(pipelineId.value ? { pipelineId: pipelineId.value } : {}),
|
|
212
242
|
...(Object.keys(agentConfigValues.value).length
|
|
213
243
|
? { agentConfig: agentConfigValues.value }
|
|
@@ -381,6 +411,21 @@ async function add() {
|
|
|
381
411
|
</UButton>
|
|
382
412
|
</UDropdownMenu>
|
|
383
413
|
</UFormField>
|
|
414
|
+
|
|
415
|
+
<UFormField label="Model preset">
|
|
416
|
+
<UDropdownMenu :items="modelPresetMenu" class="w-full">
|
|
417
|
+
<UButton
|
|
418
|
+
color="neutral"
|
|
419
|
+
variant="subtle"
|
|
420
|
+
size="sm"
|
|
421
|
+
icon="i-lucide-cpu"
|
|
422
|
+
trailing-icon="i-lucide-chevron-down"
|
|
423
|
+
class="w-full justify-between"
|
|
424
|
+
>
|
|
425
|
+
{{ selectedModelPresetLabel }}
|
|
426
|
+
</UButton>
|
|
427
|
+
</UDropdownMenu>
|
|
428
|
+
</UFormField>
|
|
384
429
|
</div>
|
|
385
430
|
|
|
386
431
|
<div v-if="configDescriptors.length" class="space-y-3">
|
|
@@ -184,12 +184,12 @@ const commands = computed<Command[]>(() => {
|
|
|
184
184
|
run: () => ui.openWorkspaceSettings(),
|
|
185
185
|
})
|
|
186
186
|
list.push({
|
|
187
|
-
id: 'model-
|
|
188
|
-
label: '
|
|
187
|
+
id: 'model-configuration',
|
|
188
|
+
label: 'Model Configuration',
|
|
189
189
|
group: 'Workspace',
|
|
190
190
|
icon: 'i-lucide-cpu',
|
|
191
|
-
keywords: 'model llm routing agent kind default',
|
|
192
|
-
run: () => ui.
|
|
191
|
+
keywords: 'model llm routing agent kind default preset configuration',
|
|
192
|
+
run: () => ui.openModelConfig(),
|
|
193
193
|
})
|
|
194
194
|
list.push({
|
|
195
195
|
id: 'service-fragment-defaults',
|
|
@@ -7,6 +7,7 @@ const props = defineProps<{ block: Block }>()
|
|
|
7
7
|
|
|
8
8
|
const board = useBoardStore()
|
|
9
9
|
const mergePresets = useMergePresetsStore()
|
|
10
|
+
const modelPresets = useModelPresetsStore()
|
|
10
11
|
const pipelines = usePipelinesStore()
|
|
11
12
|
const accounts = useAccountsStore()
|
|
12
13
|
const tracker = useTrackerStore()
|
|
@@ -66,6 +67,32 @@ function setPreset(id: string) {
|
|
|
66
67
|
board.updateBlock(props.block.id, { mergePresetId: id })
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// ---- model preset ----------------------------------------------------------
|
|
71
|
+
// Which model preset decides the model each agent step runs on. None selected → the
|
|
72
|
+
// workspace default preset. Changing it affects only steps that haven't started yet
|
|
73
|
+
// (a running step keeps the model it was dispatched with). A model pinned directly on
|
|
74
|
+
// the task still overrides the preset.
|
|
75
|
+
const selectedModelPreset = computed(() => modelPresets.resolve(props.block.modelPresetId))
|
|
76
|
+
const modelPresetMenu = computed(() => [
|
|
77
|
+
[
|
|
78
|
+
{
|
|
79
|
+
label: modelPresets.defaultPreset
|
|
80
|
+
? `Default (${modelPresets.defaultPreset.name})`
|
|
81
|
+
: 'Workspace default',
|
|
82
|
+
icon: 'i-lucide-rotate-ccw',
|
|
83
|
+
onSelect: () => setModelPreset(''),
|
|
84
|
+
},
|
|
85
|
+
...modelPresets.presets.map((p) => ({
|
|
86
|
+
label: p.name,
|
|
87
|
+
icon: 'i-lucide-cpu',
|
|
88
|
+
onSelect: () => setModelPreset(p.id),
|
|
89
|
+
})),
|
|
90
|
+
],
|
|
91
|
+
])
|
|
92
|
+
function setModelPreset(id: string) {
|
|
93
|
+
board.updateBlock(props.block.id, { modelPresetId: id })
|
|
94
|
+
}
|
|
95
|
+
|
|
69
96
|
// ---- pipeline --------------------------------------------------------------
|
|
70
97
|
// The pipeline this task's Run controls default to. None selected → the user picks
|
|
71
98
|
// at run time (the board falls back to the first defined pipeline).
|
|
@@ -189,6 +216,38 @@ const resolveOnMergeLabel = computed(() =>
|
|
|
189
216
|
</div>
|
|
190
217
|
</div>
|
|
191
218
|
|
|
219
|
+
<!-- model preset -->
|
|
220
|
+
<div>
|
|
221
|
+
<div class="mb-1 flex items-center justify-between">
|
|
222
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
223
|
+
Model preset
|
|
224
|
+
</span>
|
|
225
|
+
<UDropdownMenu :items="modelPresetMenu">
|
|
226
|
+
<UButton
|
|
227
|
+
size="xs"
|
|
228
|
+
variant="ghost"
|
|
229
|
+
color="neutral"
|
|
230
|
+
icon="i-lucide-cpu"
|
|
231
|
+
trailing-icon="i-lucide-chevron-down"
|
|
232
|
+
/>
|
|
233
|
+
</UDropdownMenu>
|
|
234
|
+
</div>
|
|
235
|
+
<div v-if="selectedModelPreset" class="text-[11px] text-slate-400">
|
|
236
|
+
<span class="text-slate-300">{{ selectedModelPreset.name }}</span>
|
|
237
|
+
— base {{ selectedModelPreset.baseModelId
|
|
238
|
+
}}<span v-if="Object.keys(selectedModelPreset.overrides).length">
|
|
239
|
+
, {{ Object.keys(selectedModelPreset.overrides).length }} override(s)</span
|
|
240
|
+
>.
|
|
241
|
+
<span v-if="!block.modelPresetId" class="text-slate-500">(workspace default)</span>
|
|
242
|
+
</div>
|
|
243
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
244
|
+
No preset configured — agents run on the deployment's default routing.
|
|
245
|
+
</div>
|
|
246
|
+
<p class="mt-1 text-[11px] text-slate-500">
|
|
247
|
+
Changing this affects only steps that haven't started yet.
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
192
251
|
<!-- issue-tracker writeback overrides -->
|
|
193
252
|
<div>
|
|
194
253
|
<div class="mb-1 flex items-center justify-between">
|
|
@@ -234,8 +234,8 @@ async function clone(p: Pipeline) {
|
|
|
234
234
|
variant="soft"
|
|
235
235
|
size="xs"
|
|
236
236
|
icon="i-lucide-cpu"
|
|
237
|
-
title="
|
|
238
|
-
@click="ui.
|
|
237
|
+
title="Manage model presets (which model each agent runs on)"
|
|
238
|
+
@click="ui.openModelConfig()"
|
|
239
239
|
>
|
|
240
240
|
Configure models
|
|
241
241
|
</UButton>
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Workspace settings: the Model Configuration screen. A workspace keeps a library of
|
|
3
|
+
// MODEL PRESETS — each a named base model applied to every agent kind plus optional
|
|
4
|
+
// per-agent overrides. A task picks a preset (or the workspace default); the chosen
|
|
5
|
+
// preset decides which model each pipeline step runs on, unless the task pins a model
|
|
6
|
+
// directly. The built-in default ("Kimi K2.7") points every agent at Kimi K2.7; a
|
|
7
|
+
// second built-in points everything at GLM-5.2.
|
|
8
|
+
//
|
|
9
|
+
// Styled as a dark full-screen window like the agent-output review overlay rather than
|
|
10
|
+
// a light modal, so the text stays readable regardless of the OS colour-mode
|
|
11
|
+
// preference. The list view shows each preset; the editor view creates/edits one with a
|
|
12
|
+
// base-model picker plus a filterable per-agent override list.
|
|
13
|
+
import { computed, ref, watch } from 'vue'
|
|
14
|
+
import { onKeyStroke } from '@vueuse/core'
|
|
15
|
+
import type { AgentKind } from '~/types/domain'
|
|
16
|
+
import type { ModelPreset } from '~/types/model-presets'
|
|
17
|
+
import { MODEL_CONFIGURABLE_SYSTEM_KINDS } from '~/utils/catalog'
|
|
18
|
+
import { contextLabel, costLabel, displayFlavor, isSelectable } from '~/stores/models'
|
|
19
|
+
|
|
20
|
+
const ui = useUiStore()
|
|
21
|
+
const models = useModelsStore()
|
|
22
|
+
const presets = useModelPresetsStore()
|
|
23
|
+
const agents = useAgentsStore()
|
|
24
|
+
const creds = useVendorCredentialsStore()
|
|
25
|
+
const workspace = useWorkspaceStore()
|
|
26
|
+
const toast = useToast()
|
|
27
|
+
|
|
28
|
+
const open = computed({
|
|
29
|
+
get: () => ui.modelConfigOpen,
|
|
30
|
+
set: (v: boolean) => (v ? ui.openModelConfig() : ui.closeModelConfig()),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// null = the preset list; an object = the create/edit form for one preset.
|
|
34
|
+
interface EditorState {
|
|
35
|
+
id?: string
|
|
36
|
+
name: string
|
|
37
|
+
baseModelId: string
|
|
38
|
+
overrides: Record<string, string>
|
|
39
|
+
isDefault: boolean
|
|
40
|
+
}
|
|
41
|
+
const editor = ref<EditorState | null>(null)
|
|
42
|
+
const busy = ref(false)
|
|
43
|
+
// Narrows the agent-kind override rows, for finding a kind fast in a long catalog.
|
|
44
|
+
const filter = ref('')
|
|
45
|
+
|
|
46
|
+
// The palette archetypes PLUS the engine-driven kinds that still run an LLM
|
|
47
|
+
// (spec-writer, merger, the fixers/resolver). The pure gates run no model, so they
|
|
48
|
+
// stay out — exactly the set the per-agent override list should cover.
|
|
49
|
+
const configurableKinds = computed(() => [...agents.archetypes, ...MODEL_CONFIGURABLE_SYSTEM_KINDS])
|
|
50
|
+
const filteredKinds = computed(() => {
|
|
51
|
+
const q = filter.value.trim().toLowerCase()
|
|
52
|
+
if (!q) return configurableKinds.value
|
|
53
|
+
return configurableKinds.value.filter(
|
|
54
|
+
(a) => a.label.toLowerCase().includes(q) || String(a.kind).toLowerCase().includes(q),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
watch(open, (isOpen) => {
|
|
59
|
+
if (isOpen) {
|
|
60
|
+
editor.value = null
|
|
61
|
+
filter.value = ''
|
|
62
|
+
void models.ensureLoaded(workspace.workspaceId ?? undefined)
|
|
63
|
+
if (workspace.workspaceId) void creds.load(workspace.workspaceId)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
onKeyStroke('Escape', () => {
|
|
68
|
+
if (!open.value) return
|
|
69
|
+
// Esc backs out of the editor first, then closes the panel.
|
|
70
|
+
if (editor.value) editor.value = null
|
|
71
|
+
else open.value = false
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/** The selectable catalog models, as `{ id, label, suffix }` for a dropdown. */
|
|
75
|
+
const selectableModels = computed(() => {
|
|
76
|
+
const configured = creds.configuredVendors
|
|
77
|
+
return models.models
|
|
78
|
+
.filter((m) => isSelectable(m, configured))
|
|
79
|
+
.map((m) => {
|
|
80
|
+
const flavor = displayFlavor(m, configured)
|
|
81
|
+
const ctx = contextLabel(flavor.contextTokens)
|
|
82
|
+
const price = costLabel(flavor) ?? (flavor.quotaBased ? 'quota' : undefined)
|
|
83
|
+
const suffix = [flavor.providerLabel, ctx, price].filter(Boolean).join(' · ')
|
|
84
|
+
return {
|
|
85
|
+
id: m.id,
|
|
86
|
+
label: m.label,
|
|
87
|
+
suffix,
|
|
88
|
+
icon: flavor.quotaBased ? 'i-lucide-infinity' : 'i-lucide-cpu',
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/** A readable label for a model id (catalog label + provider, else the raw id). */
|
|
94
|
+
function modelLabel(id: string | undefined): string {
|
|
95
|
+
if (!id) return '—'
|
|
96
|
+
const m = models.getModel(id)
|
|
97
|
+
if (!m) return id
|
|
98
|
+
return `${m.label} · ${displayFlavor(m, creds.configuredVendors).providerLabel}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- preset list -----------------------------------------------------------
|
|
102
|
+
const sortedPresets = computed(() => [...presets.presets].sort((a, b) => a.createdAt - b.createdAt))
|
|
103
|
+
|
|
104
|
+
function startCreate() {
|
|
105
|
+
editor.value = {
|
|
106
|
+
name: '',
|
|
107
|
+
baseModelId: selectableModels.value[0]?.id ?? 'kimi-k2.7',
|
|
108
|
+
overrides: {},
|
|
109
|
+
isDefault: false,
|
|
110
|
+
}
|
|
111
|
+
filter.value = ''
|
|
112
|
+
}
|
|
113
|
+
function startEdit(p: ModelPreset) {
|
|
114
|
+
editor.value = {
|
|
115
|
+
id: p.id,
|
|
116
|
+
name: p.name,
|
|
117
|
+
baseModelId: p.baseModelId,
|
|
118
|
+
overrides: { ...p.overrides },
|
|
119
|
+
isDefault: p.isDefault,
|
|
120
|
+
}
|
|
121
|
+
filter.value = ''
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function setDefault(p: ModelPreset) {
|
|
125
|
+
if (p.isDefault) return
|
|
126
|
+
busy.value = true
|
|
127
|
+
try {
|
|
128
|
+
await presets.update(p.id, { isDefault: true })
|
|
129
|
+
} catch (e) {
|
|
130
|
+
fail('Could not set the default preset', e)
|
|
131
|
+
} finally {
|
|
132
|
+
busy.value = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function remove(p: ModelPreset) {
|
|
137
|
+
busy.value = true
|
|
138
|
+
try {
|
|
139
|
+
await presets.remove(p.id)
|
|
140
|
+
} catch (e) {
|
|
141
|
+
fail('Could not delete the preset', e)
|
|
142
|
+
} finally {
|
|
143
|
+
busy.value = false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- editor ----------------------------------------------------------------
|
|
148
|
+
/** The base-model dropdown items. */
|
|
149
|
+
const baseMenu = computed(() => [
|
|
150
|
+
selectableModels.value.map((m) => ({
|
|
151
|
+
label: `${m.label} · ${m.suffix}`,
|
|
152
|
+
icon: m.icon,
|
|
153
|
+
onSelect: () => {
|
|
154
|
+
if (editor.value) editor.value.baseModelId = m.id
|
|
155
|
+
},
|
|
156
|
+
})),
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
/** A per-kind override dropdown: "Use base model" reset plus the catalog. */
|
|
160
|
+
function overrideMenu(kind: AgentKind) {
|
|
161
|
+
return [
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
label: 'Use base model',
|
|
165
|
+
icon: 'i-lucide-rotate-ccw',
|
|
166
|
+
onSelect: () => setOverride(kind, null),
|
|
167
|
+
},
|
|
168
|
+
...selectableModels.value.map((m) => ({
|
|
169
|
+
label: `${m.label} · ${m.suffix}`,
|
|
170
|
+
icon: m.icon,
|
|
171
|
+
onSelect: () => setOverride(kind, m.id),
|
|
172
|
+
})),
|
|
173
|
+
],
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
function setOverride(kind: AgentKind, modelId: string | null) {
|
|
177
|
+
if (!editor.value) return
|
|
178
|
+
const next = { ...editor.value.overrides }
|
|
179
|
+
if (modelId) next[kind] = modelId
|
|
180
|
+
else delete next[kind]
|
|
181
|
+
editor.value.overrides = next
|
|
182
|
+
}
|
|
183
|
+
/** The label on a kind's override button: its override model, else "Base model". */
|
|
184
|
+
function overrideLabel(kind: AgentKind): string {
|
|
185
|
+
const id = editor.value?.overrides[kind]
|
|
186
|
+
return id ? modelLabel(id) : 'Base model'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function save() {
|
|
190
|
+
const e = editor.value
|
|
191
|
+
if (!e) return
|
|
192
|
+
if (!e.name.trim()) {
|
|
193
|
+
fail('Name required', new Error('Give the preset a name.'))
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
busy.value = true
|
|
197
|
+
try {
|
|
198
|
+
if (e.id) {
|
|
199
|
+
await presets.update(e.id, {
|
|
200
|
+
name: e.name.trim(),
|
|
201
|
+
baseModelId: e.baseModelId,
|
|
202
|
+
overrides: e.overrides,
|
|
203
|
+
isDefault: e.isDefault,
|
|
204
|
+
})
|
|
205
|
+
} else {
|
|
206
|
+
await presets.create({
|
|
207
|
+
name: e.name.trim(),
|
|
208
|
+
baseModelId: e.baseModelId,
|
|
209
|
+
overrides: e.overrides,
|
|
210
|
+
isDefault: e.isDefault,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
editor.value = null
|
|
214
|
+
} catch (err) {
|
|
215
|
+
fail('Could not save the preset', err)
|
|
216
|
+
} finally {
|
|
217
|
+
busy.value = false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function fail(title: string, e: unknown) {
|
|
222
|
+
toast.add({
|
|
223
|
+
title,
|
|
224
|
+
description: e instanceof Error ? e.message : String(e),
|
|
225
|
+
icon: 'i-lucide-triangle-alert',
|
|
226
|
+
color: 'error',
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<template>
|
|
232
|
+
<Teleport to="body">
|
|
233
|
+
<Transition name="reader-fade">
|
|
234
|
+
<div
|
|
235
|
+
v-if="open"
|
|
236
|
+
class="fixed inset-0 z-50 flex flex-col bg-slate-950/96 backdrop-blur-sm"
|
|
237
|
+
role="dialog"
|
|
238
|
+
aria-modal="true"
|
|
239
|
+
>
|
|
240
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
|
|
241
|
+
<div
|
|
242
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500/15"
|
|
243
|
+
>
|
|
244
|
+
<UIcon name="i-lucide-cpu" class="h-5 w-5 text-indigo-300" />
|
|
245
|
+
</div>
|
|
246
|
+
<div class="min-w-0">
|
|
247
|
+
<h1 class="truncate text-base font-semibold text-white">Model Configuration</h1>
|
|
248
|
+
<p class="truncate text-xs text-slate-500">
|
|
249
|
+
Presets that map a model to every agent. A task picks one; tasks default to the
|
|
250
|
+
workspace default preset.
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
<UButton
|
|
254
|
+
v-if="editor"
|
|
255
|
+
icon="i-lucide-arrow-left"
|
|
256
|
+
color="neutral"
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="sm"
|
|
259
|
+
class="ml-auto"
|
|
260
|
+
@click="editor = null"
|
|
261
|
+
>
|
|
262
|
+
Back
|
|
263
|
+
</UButton>
|
|
264
|
+
<UButton
|
|
265
|
+
icon="i-lucide-x"
|
|
266
|
+
color="neutral"
|
|
267
|
+
variant="ghost"
|
|
268
|
+
size="sm"
|
|
269
|
+
:class="editor ? '' : 'ml-auto'"
|
|
270
|
+
title="Close (Esc)"
|
|
271
|
+
@click="open = false"
|
|
272
|
+
/>
|
|
273
|
+
</header>
|
|
274
|
+
|
|
275
|
+
<div class="flex-1 overflow-auto px-6 py-6">
|
|
276
|
+
<div class="mx-auto max-w-3xl space-y-5">
|
|
277
|
+
<!-- ===== list view ===== -->
|
|
278
|
+
<template v-if="!editor">
|
|
279
|
+
<div class="flex items-center justify-between">
|
|
280
|
+
<p class="text-sm leading-relaxed text-slate-400">
|
|
281
|
+
Each preset sets a <span class="text-slate-300">base model</span> for every agent,
|
|
282
|
+
with optional per-agent overrides. A model pinned on an individual task still
|
|
283
|
+
overrides the preset.
|
|
284
|
+
</p>
|
|
285
|
+
<UButton
|
|
286
|
+
icon="i-lucide-plus"
|
|
287
|
+
color="primary"
|
|
288
|
+
size="sm"
|
|
289
|
+
class="shrink-0"
|
|
290
|
+
@click="startCreate"
|
|
291
|
+
>
|
|
292
|
+
New preset
|
|
293
|
+
</UButton>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<p v-if="models.models.length === 0" class="py-4 text-center text-sm text-slate-500">
|
|
297
|
+
Loading model catalog…
|
|
298
|
+
</p>
|
|
299
|
+
|
|
300
|
+
<div v-else class="space-y-3">
|
|
301
|
+
<div
|
|
302
|
+
v-for="p in sortedPresets"
|
|
303
|
+
:key="p.id"
|
|
304
|
+
class="rounded-xl border border-slate-800 bg-slate-900/50 p-4"
|
|
305
|
+
>
|
|
306
|
+
<div class="flex items-center gap-2">
|
|
307
|
+
<span class="truncate text-sm font-semibold text-slate-100">{{ p.name }}</span>
|
|
308
|
+
<UBadge v-if="p.isDefault" color="primary" variant="subtle" size="xs">
|
|
309
|
+
Default
|
|
310
|
+
</UBadge>
|
|
311
|
+
<div class="ml-auto flex items-center gap-1">
|
|
312
|
+
<UButton
|
|
313
|
+
v-if="!p.isDefault"
|
|
314
|
+
size="xs"
|
|
315
|
+
variant="ghost"
|
|
316
|
+
color="neutral"
|
|
317
|
+
icon="i-lucide-star"
|
|
318
|
+
:loading="busy"
|
|
319
|
+
title="Set as workspace default"
|
|
320
|
+
@click="setDefault(p)"
|
|
321
|
+
/>
|
|
322
|
+
<UButton
|
|
323
|
+
size="xs"
|
|
324
|
+
variant="ghost"
|
|
325
|
+
color="neutral"
|
|
326
|
+
icon="i-lucide-pencil"
|
|
327
|
+
title="Edit preset"
|
|
328
|
+
@click="startEdit(p)"
|
|
329
|
+
/>
|
|
330
|
+
<UButton
|
|
331
|
+
size="xs"
|
|
332
|
+
variant="ghost"
|
|
333
|
+
color="error"
|
|
334
|
+
icon="i-lucide-trash-2"
|
|
335
|
+
:disabled="p.isDefault"
|
|
336
|
+
:loading="busy"
|
|
337
|
+
:title="
|
|
338
|
+
p.isDefault ? 'The default preset cannot be deleted' : 'Delete preset'
|
|
339
|
+
"
|
|
340
|
+
@click="remove(p)"
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="mt-1.5 text-[11px] text-slate-400">
|
|
345
|
+
Base: <span class="text-slate-300">{{ modelLabel(p.baseModelId) }}</span>
|
|
346
|
+
<span v-if="Object.keys(p.overrides).length">
|
|
347
|
+
· {{ Object.keys(p.overrides).length }} override<span
|
|
348
|
+
v-if="Object.keys(p.overrides).length !== 1"
|
|
349
|
+
>s</span
|
|
350
|
+
>
|
|
351
|
+
</span>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
<p
|
|
355
|
+
v-if="sortedPresets.length === 0"
|
|
356
|
+
class="py-6 text-center text-sm text-slate-500"
|
|
357
|
+
>
|
|
358
|
+
No presets yet — create one to map models to your agents.
|
|
359
|
+
</p>
|
|
360
|
+
</div>
|
|
361
|
+
</template>
|
|
362
|
+
|
|
363
|
+
<!-- ===== editor view ===== -->
|
|
364
|
+
<template v-else>
|
|
365
|
+
<div class="space-y-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
|
366
|
+
<div>
|
|
367
|
+
<label
|
|
368
|
+
class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
369
|
+
>
|
|
370
|
+
Name
|
|
371
|
+
</label>
|
|
372
|
+
<UInput
|
|
373
|
+
v-model="editor.name"
|
|
374
|
+
placeholder="e.g. Kimi K2.7"
|
|
375
|
+
size="sm"
|
|
376
|
+
class="w-full"
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div>
|
|
381
|
+
<label
|
|
382
|
+
class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
383
|
+
>
|
|
384
|
+
Base model (applied to every agent)
|
|
385
|
+
</label>
|
|
386
|
+
<UDropdownMenu
|
|
387
|
+
:items="baseMenu"
|
|
388
|
+
:ui="{ content: 'max-h-80 overflow-y-auto z-[60]' }"
|
|
389
|
+
>
|
|
390
|
+
<UButton
|
|
391
|
+
size="sm"
|
|
392
|
+
color="primary"
|
|
393
|
+
variant="subtle"
|
|
394
|
+
trailing-icon="i-lucide-chevron-down"
|
|
395
|
+
class="w-full justify-between"
|
|
396
|
+
>
|
|
397
|
+
<span class="truncate">{{ modelLabel(editor.baseModelId) }}</span>
|
|
398
|
+
</UButton>
|
|
399
|
+
</UDropdownMenu>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<label class="flex items-center gap-2 text-sm text-slate-300">
|
|
403
|
+
<UCheckbox v-model="editor.isDefault" />
|
|
404
|
+
Make this the workspace default
|
|
405
|
+
</label>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div>
|
|
409
|
+
<div class="mb-1 flex items-center justify-between">
|
|
410
|
+
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
411
|
+
Per-agent overrides
|
|
412
|
+
</span>
|
|
413
|
+
</div>
|
|
414
|
+
<UInput
|
|
415
|
+
v-model="filter"
|
|
416
|
+
icon="i-lucide-search"
|
|
417
|
+
size="sm"
|
|
418
|
+
placeholder="Filter agents…"
|
|
419
|
+
class="mb-3 w-full"
|
|
420
|
+
/>
|
|
421
|
+
<div
|
|
422
|
+
class="divide-y divide-slate-800 rounded-xl border border-slate-800 bg-slate-900/50"
|
|
423
|
+
>
|
|
424
|
+
<div
|
|
425
|
+
v-for="a in filteredKinds"
|
|
426
|
+
:key="a.kind"
|
|
427
|
+
class="flex items-center gap-3 px-4 py-3"
|
|
428
|
+
>
|
|
429
|
+
<UIcon
|
|
430
|
+
:name="a.icon"
|
|
431
|
+
class="h-4 w-4 shrink-0"
|
|
432
|
+
:style="{ color: a.color }"
|
|
433
|
+
:title="a.description"
|
|
434
|
+
/>
|
|
435
|
+
<div class="min-w-0 flex-1" :title="a.description">
|
|
436
|
+
<p class="truncate text-sm text-slate-200">{{ a.label }}</p>
|
|
437
|
+
</div>
|
|
438
|
+
<UDropdownMenu
|
|
439
|
+
:items="overrideMenu(a.kind)"
|
|
440
|
+
:ui="{ content: 'max-h-80 overflow-y-auto z-[60]' }"
|
|
441
|
+
>
|
|
442
|
+
<UButton
|
|
443
|
+
size="xs"
|
|
444
|
+
:color="editor.overrides[a.kind] ? 'primary' : 'neutral'"
|
|
445
|
+
:variant="editor.overrides[a.kind] ? 'subtle' : 'soft'"
|
|
446
|
+
trailing-icon="i-lucide-chevron-down"
|
|
447
|
+
class="w-64 shrink-0 justify-between"
|
|
448
|
+
>
|
|
449
|
+
<span class="truncate">{{ overrideLabel(a.kind) }}</span>
|
|
450
|
+
</UButton>
|
|
451
|
+
</UDropdownMenu>
|
|
452
|
+
</div>
|
|
453
|
+
<p
|
|
454
|
+
v-if="filteredKinds.length === 0"
|
|
455
|
+
class="px-4 py-6 text-center text-sm text-slate-500"
|
|
456
|
+
>
|
|
457
|
+
No agents match "{{ filter }}".
|
|
458
|
+
</p>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div class="flex items-center justify-end gap-2">
|
|
463
|
+
<UButton color="neutral" variant="ghost" size="sm" @click="editor = null">
|
|
464
|
+
Cancel
|
|
465
|
+
</UButton>
|
|
466
|
+
<UButton color="primary" size="sm" :loading="busy" @click="save">
|
|
467
|
+
{{ editor.id ? 'Save changes' : 'Create preset' }}
|
|
468
|
+
</UButton>
|
|
469
|
+
</div>
|
|
470
|
+
</template>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</Transition>
|
|
475
|
+
</Teleport>
|
|
476
|
+
</template>
|
|
477
|
+
|
|
478
|
+
<style scoped>
|
|
479
|
+
.reader-fade-enter-active,
|
|
480
|
+
.reader-fade-leave-active {
|
|
481
|
+
transition: opacity 0.18s ease;
|
|
482
|
+
}
|
|
483
|
+
.reader-fade-enter-from,
|
|
484
|
+
.reader-fade-leave-to {
|
|
485
|
+
opacity: 0;
|
|
486
|
+
}
|
|
487
|
+
</style>
|