@cat-factory/app 0.6.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/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Workspace settings: the merge-threshold preset library a task picks its
|
|
3
|
+
// auto-merge policy from (the `merger` step compares a PR's assessment against the
|
|
4
|
+
// resolved preset). Full CRUD over the mergePresets store — the same library the
|
|
5
|
+
// task inspector's "Merge policy" dropdown selects from. Exactly one preset is the
|
|
6
|
+
// default; it cannot be deleted or un-defaulted (the backend enforces this too).
|
|
7
|
+
import { reactive, ref, watch } from 'vue'
|
|
8
|
+
import type { MergeThresholdPreset, RequirementConcernLevel } from '~/types/merge'
|
|
9
|
+
|
|
10
|
+
// Concern-level options for the requirements auto-pass threshold (none < low < medium < high).
|
|
11
|
+
const CONCERN_LEVELS: { value: RequirementConcernLevel; label: string }[] = [
|
|
12
|
+
{ value: 'none', label: 'None (always stop)' },
|
|
13
|
+
{ value: 'low', label: 'Low' },
|
|
14
|
+
{ value: 'medium', label: 'Medium' },
|
|
15
|
+
{ value: 'high', label: 'High (never stop)' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const ui = useUiStore()
|
|
19
|
+
const store = useMergePresetsStore()
|
|
20
|
+
const toast = useToast()
|
|
21
|
+
|
|
22
|
+
const open = computed({
|
|
23
|
+
get: () => ui.mergeThresholdsOpen,
|
|
24
|
+
set: (v: boolean) => (v ? ui.openMergeThresholds() : ui.closeMergeThresholds()),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Local editable copy per preset, kept in sync with the store. Percentages are
|
|
28
|
+
// edited 0..100 and stored 0..1.
|
|
29
|
+
interface Draft {
|
|
30
|
+
name: string
|
|
31
|
+
maxComplexity: number
|
|
32
|
+
maxRisk: number
|
|
33
|
+
maxImpact: number
|
|
34
|
+
ciMaxAttempts: number
|
|
35
|
+
maxRequirementIterations: number
|
|
36
|
+
maxRequirementConcernAllowed: RequirementConcernLevel
|
|
37
|
+
}
|
|
38
|
+
const drafts = reactive<Record<string, Draft>>({})
|
|
39
|
+
|
|
40
|
+
function toDraft(p: MergeThresholdPreset): Draft {
|
|
41
|
+
return {
|
|
42
|
+
name: p.name,
|
|
43
|
+
maxComplexity: Math.round(p.maxComplexity * 100),
|
|
44
|
+
maxRisk: Math.round(p.maxRisk * 100),
|
|
45
|
+
maxImpact: Math.round(p.maxImpact * 100),
|
|
46
|
+
ciMaxAttempts: p.ciMaxAttempts,
|
|
47
|
+
maxRequirementIterations: p.maxRequirementIterations,
|
|
48
|
+
maxRequirementConcernAllowed: p.maxRequirementConcernAllowed,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
watch(
|
|
53
|
+
() => store.presets,
|
|
54
|
+
(presets) => {
|
|
55
|
+
for (const p of presets) if (!drafts[p.id]) drafts[p.id] = toDraft(p)
|
|
56
|
+
for (const id of Object.keys(drafts)) if (!presets.some((p) => p.id === id)) delete drafts[id]
|
|
57
|
+
},
|
|
58
|
+
{ immediate: true, deep: false },
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const busy = ref<string | null>(null)
|
|
62
|
+
|
|
63
|
+
function notifyError(title: string, e: unknown) {
|
|
64
|
+
toast.add({
|
|
65
|
+
title,
|
|
66
|
+
description: e instanceof Error ? e.message : String(e),
|
|
67
|
+
icon: 'i-lucide-triangle-alert',
|
|
68
|
+
color: 'error',
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function save(p: MergeThresholdPreset) {
|
|
73
|
+
const d = drafts[p.id]
|
|
74
|
+
if (!d) return
|
|
75
|
+
busy.value = p.id
|
|
76
|
+
try {
|
|
77
|
+
await store.update(p.id, {
|
|
78
|
+
name: d.name.trim() || p.name,
|
|
79
|
+
maxComplexity: d.maxComplexity / 100,
|
|
80
|
+
maxRisk: d.maxRisk / 100,
|
|
81
|
+
maxImpact: d.maxImpact / 100,
|
|
82
|
+
ciMaxAttempts: d.ciMaxAttempts,
|
|
83
|
+
maxRequirementIterations: d.maxRequirementIterations,
|
|
84
|
+
maxRequirementConcernAllowed: d.maxRequirementConcernAllowed,
|
|
85
|
+
})
|
|
86
|
+
toast.add({ title: 'Preset saved', icon: 'i-lucide-check', color: 'success' })
|
|
87
|
+
} catch (e) {
|
|
88
|
+
notifyError('Could not save preset', e)
|
|
89
|
+
} finally {
|
|
90
|
+
busy.value = null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function makeDefault(p: MergeThresholdPreset) {
|
|
95
|
+
busy.value = p.id
|
|
96
|
+
try {
|
|
97
|
+
await store.update(p.id, { isDefault: true })
|
|
98
|
+
} catch (e) {
|
|
99
|
+
notifyError('Could not set default', e)
|
|
100
|
+
} finally {
|
|
101
|
+
busy.value = null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function remove(p: MergeThresholdPreset) {
|
|
106
|
+
busy.value = p.id
|
|
107
|
+
try {
|
|
108
|
+
await store.remove(p.id)
|
|
109
|
+
} catch (e) {
|
|
110
|
+
notifyError('Could not delete preset', e)
|
|
111
|
+
} finally {
|
|
112
|
+
busy.value = null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- create form ----------------------------------------------------------
|
|
117
|
+
const creating = ref(false)
|
|
118
|
+
const draft = reactive<Draft>({
|
|
119
|
+
name: '',
|
|
120
|
+
maxComplexity: 50,
|
|
121
|
+
maxRisk: 40,
|
|
122
|
+
maxImpact: 50,
|
|
123
|
+
ciMaxAttempts: 10,
|
|
124
|
+
maxRequirementIterations: 6,
|
|
125
|
+
maxRequirementConcernAllowed: 'none',
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
async function create() {
|
|
129
|
+
if (!draft.name.trim()) return
|
|
130
|
+
creating.value = true
|
|
131
|
+
try {
|
|
132
|
+
await store.create({
|
|
133
|
+
name: draft.name.trim(),
|
|
134
|
+
maxComplexity: draft.maxComplexity / 100,
|
|
135
|
+
maxRisk: draft.maxRisk / 100,
|
|
136
|
+
maxImpact: draft.maxImpact / 100,
|
|
137
|
+
ciMaxAttempts: draft.ciMaxAttempts,
|
|
138
|
+
maxRequirementIterations: draft.maxRequirementIterations,
|
|
139
|
+
maxRequirementConcernAllowed: draft.maxRequirementConcernAllowed,
|
|
140
|
+
})
|
|
141
|
+
draft.name = ''
|
|
142
|
+
toast.add({ title: 'Preset created', icon: 'i-lucide-check', color: 'success' })
|
|
143
|
+
} catch (e) {
|
|
144
|
+
notifyError('Could not create preset', e)
|
|
145
|
+
} finally {
|
|
146
|
+
creating.value = false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<UModal v-model:open="open" title="Merge thresholds" :ui="{ content: 'max-w-2xl' }">
|
|
153
|
+
<template #body>
|
|
154
|
+
<div class="space-y-4">
|
|
155
|
+
<p class="text-xs text-slate-400">
|
|
156
|
+
Named auto-merge policies a task can choose. After CI passes, the
|
|
157
|
+
<span class="text-slate-300">merger</span> agent scores the PR on complexity, risk and
|
|
158
|
+
impact (0–100%); the PR auto-merges only when every score is at or below the preset's
|
|
159
|
+
ceilings — otherwise a review notification is raised. The default preset governs any task
|
|
160
|
+
that picks none.
|
|
161
|
+
</p>
|
|
162
|
+
|
|
163
|
+
<div
|
|
164
|
+
v-for="p in store.presets"
|
|
165
|
+
:key="p.id"
|
|
166
|
+
class="rounded-lg border border-slate-700 bg-slate-800/40 p-3"
|
|
167
|
+
>
|
|
168
|
+
<div class="mb-3 flex items-center gap-2">
|
|
169
|
+
<UInput
|
|
170
|
+
v-model="drafts[p.id]!.name"
|
|
171
|
+
size="sm"
|
|
172
|
+
class="flex-1"
|
|
173
|
+
placeholder="Preset name"
|
|
174
|
+
/>
|
|
175
|
+
<UBadge v-if="p.isDefault" color="primary" variant="subtle" size="sm">Default</UBadge>
|
|
176
|
+
<UButton
|
|
177
|
+
v-else
|
|
178
|
+
color="neutral"
|
|
179
|
+
variant="ghost"
|
|
180
|
+
size="xs"
|
|
181
|
+
icon="i-lucide-star"
|
|
182
|
+
:loading="busy === p.id"
|
|
183
|
+
@click="makeDefault(p)"
|
|
184
|
+
>
|
|
185
|
+
Make default
|
|
186
|
+
</UButton>
|
|
187
|
+
<UButton
|
|
188
|
+
color="error"
|
|
189
|
+
variant="ghost"
|
|
190
|
+
size="xs"
|
|
191
|
+
icon="i-lucide-trash-2"
|
|
192
|
+
:disabled="p.isDefault || busy === p.id"
|
|
193
|
+
:title="p.isDefault ? 'The default preset cannot be deleted' : 'Delete preset'"
|
|
194
|
+
@click="remove(p)"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
199
|
+
<label class="block">
|
|
200
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
201
|
+
Max complexity %
|
|
202
|
+
</span>
|
|
203
|
+
<UInput
|
|
204
|
+
v-model.number="drafts[p.id]!.maxComplexity"
|
|
205
|
+
type="number"
|
|
206
|
+
:min="0"
|
|
207
|
+
:max="100"
|
|
208
|
+
size="sm"
|
|
209
|
+
/>
|
|
210
|
+
</label>
|
|
211
|
+
<label class="block">
|
|
212
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
213
|
+
Max risk %
|
|
214
|
+
</span>
|
|
215
|
+
<UInput
|
|
216
|
+
v-model.number="drafts[p.id]!.maxRisk"
|
|
217
|
+
type="number"
|
|
218
|
+
:min="0"
|
|
219
|
+
:max="100"
|
|
220
|
+
size="sm"
|
|
221
|
+
/>
|
|
222
|
+
</label>
|
|
223
|
+
<label class="block">
|
|
224
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
225
|
+
Max impact %
|
|
226
|
+
</span>
|
|
227
|
+
<UInput
|
|
228
|
+
v-model.number="drafts[p.id]!.maxImpact"
|
|
229
|
+
type="number"
|
|
230
|
+
:min="0"
|
|
231
|
+
:max="100"
|
|
232
|
+
size="sm"
|
|
233
|
+
/>
|
|
234
|
+
</label>
|
|
235
|
+
<label class="block">
|
|
236
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
237
|
+
CI-fix attempts
|
|
238
|
+
</span>
|
|
239
|
+
<UInput
|
|
240
|
+
v-model.number="drafts[p.id]!.ciMaxAttempts"
|
|
241
|
+
type="number"
|
|
242
|
+
:min="0"
|
|
243
|
+
:max="50"
|
|
244
|
+
size="sm"
|
|
245
|
+
/>
|
|
246
|
+
</label>
|
|
247
|
+
<label class="block">
|
|
248
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
249
|
+
Requirement iterations
|
|
250
|
+
</span>
|
|
251
|
+
<UInput
|
|
252
|
+
v-model.number="drafts[p.id]!.maxRequirementIterations"
|
|
253
|
+
type="number"
|
|
254
|
+
:min="1"
|
|
255
|
+
:max="20"
|
|
256
|
+
size="sm"
|
|
257
|
+
/>
|
|
258
|
+
</label>
|
|
259
|
+
<label class="block">
|
|
260
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
261
|
+
Auto-pass concerns ≤
|
|
262
|
+
</span>
|
|
263
|
+
<USelect
|
|
264
|
+
v-model="drafts[p.id]!.maxRequirementConcernAllowed"
|
|
265
|
+
:items="CONCERN_LEVELS"
|
|
266
|
+
value-key="value"
|
|
267
|
+
size="sm"
|
|
268
|
+
/>
|
|
269
|
+
</label>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="mt-3 flex justify-end">
|
|
273
|
+
<UButton
|
|
274
|
+
color="primary"
|
|
275
|
+
variant="soft"
|
|
276
|
+
size="xs"
|
|
277
|
+
icon="i-lucide-save"
|
|
278
|
+
:loading="busy === p.id"
|
|
279
|
+
@click="save(p)"
|
|
280
|
+
>
|
|
281
|
+
Save
|
|
282
|
+
</UButton>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<!-- create -->
|
|
287
|
+
<div class="rounded-lg border border-dashed border-slate-700 p-3">
|
|
288
|
+
<p class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
289
|
+
New preset
|
|
290
|
+
</p>
|
|
291
|
+
<div class="flex flex-wrap items-end gap-3">
|
|
292
|
+
<label class="block min-w-40 flex-1">
|
|
293
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
294
|
+
>Name</span
|
|
295
|
+
>
|
|
296
|
+
<UInput v-model="draft.name" size="sm" placeholder="e.g. Cautious" />
|
|
297
|
+
</label>
|
|
298
|
+
<label class="block w-20">
|
|
299
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
300
|
+
>Cmplx%</span
|
|
301
|
+
>
|
|
302
|
+
<UInput
|
|
303
|
+
v-model.number="draft.maxComplexity"
|
|
304
|
+
type="number"
|
|
305
|
+
:min="0"
|
|
306
|
+
:max="100"
|
|
307
|
+
size="sm"
|
|
308
|
+
/>
|
|
309
|
+
</label>
|
|
310
|
+
<label class="block w-20">
|
|
311
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
312
|
+
>Risk%</span
|
|
313
|
+
>
|
|
314
|
+
<UInput v-model.number="draft.maxRisk" type="number" :min="0" :max="100" size="sm" />
|
|
315
|
+
</label>
|
|
316
|
+
<label class="block w-20">
|
|
317
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
318
|
+
>Impact%</span
|
|
319
|
+
>
|
|
320
|
+
<UInput
|
|
321
|
+
v-model.number="draft.maxImpact"
|
|
322
|
+
type="number"
|
|
323
|
+
:min="0"
|
|
324
|
+
:max="100"
|
|
325
|
+
size="sm"
|
|
326
|
+
/>
|
|
327
|
+
</label>
|
|
328
|
+
<label class="block w-20">
|
|
329
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
330
|
+
>CI-fix</span
|
|
331
|
+
>
|
|
332
|
+
<UInput
|
|
333
|
+
v-model.number="draft.ciMaxAttempts"
|
|
334
|
+
type="number"
|
|
335
|
+
:min="0"
|
|
336
|
+
:max="50"
|
|
337
|
+
size="sm"
|
|
338
|
+
/>
|
|
339
|
+
</label>
|
|
340
|
+
<label class="block w-20">
|
|
341
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
342
|
+
>Req iter</span
|
|
343
|
+
>
|
|
344
|
+
<UInput
|
|
345
|
+
v-model.number="draft.maxRequirementIterations"
|
|
346
|
+
type="number"
|
|
347
|
+
:min="1"
|
|
348
|
+
:max="20"
|
|
349
|
+
size="sm"
|
|
350
|
+
/>
|
|
351
|
+
</label>
|
|
352
|
+
<label class="block w-32">
|
|
353
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500"
|
|
354
|
+
>Auto-pass ≤</span
|
|
355
|
+
>
|
|
356
|
+
<USelect
|
|
357
|
+
v-model="draft.maxRequirementConcernAllowed"
|
|
358
|
+
:items="CONCERN_LEVELS"
|
|
359
|
+
value-key="value"
|
|
360
|
+
size="sm"
|
|
361
|
+
/>
|
|
362
|
+
</label>
|
|
363
|
+
<UButton
|
|
364
|
+
color="primary"
|
|
365
|
+
size="sm"
|
|
366
|
+
icon="i-lucide-plus"
|
|
367
|
+
:loading="creating"
|
|
368
|
+
:disabled="!draft.name.trim()"
|
|
369
|
+
@click="create"
|
|
370
|
+
>
|
|
371
|
+
Add
|
|
372
|
+
</UButton>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</template>
|
|
377
|
+
</UModal>
|
|
378
|
+
</template>
|
|
@@ -0,0 +1,250 @@
|
|
|
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>
|